<?php

namespace mysal;

/*
 * mysqladminlite
 * 开源的 PHP 在线 MySQL 管理工具（单文件绿色版）
 * 程序特性：
 *      1. 单文件绿色版,部署超简单
 *      2. 支持分布式服务器环境下使用，支持用户登录验证、IP验证等多种权限验证功能
 *      3. 支持多个数据库服务器环境切换
 *      4. 支持查看所有数据库、表、存储过程、函数、视图等，支持查看数据表、视图、存储程序等对象结构
 *      5. 支持查看数据库引擎变量信息和数据表元数据信息
 *      6. 支持在线查看数据表结构模型信息或者将数据表结构模型导出为markdown文档，excel文档，html文档等
 *      7. 支持查询结果导出为 markdown文档，excel文档，sql文档等
 *      8. 支持最新 MySQL8.x
 *      9. 支持命令行环境使用，命令行环境支持数据迁移和测试数据生成以及数据库增删改查操作，支持导出文件，支持导入 SQL 文件
 * 说明：写这个程序主要是为了辅助项目开发中的数据库记录查看和数据库管理。这个小程序提供了程序特性描述的功能（足以应付项目开发需要）。
 *      如果您有兴趣一起完善这个小程序，欢迎 Pull Requests！感谢~
 * 项目周期：
 *      2017-8-23 立项开发
 *      2017-8-24 程序完成
 * 版权：不限商业用途! 如果您修改代码请保留原著署名，并把修改后的代码回馈到作者邮箱：<14507247@qq.com>，促进本产品进步。
 *      作者博客：http://www.cnblogs.com/sochishun/ 欢迎交流~
 *
 * 使用方法
 *      0. 依赖环境：PHP(5.6,7.0,7.1,7.3,7.4), mysql(5.7,5.8), PDO 扩展。
 *      1. 复制本文件到 web 目录。
 *      2. 首次使用请修改 JWTLite::$AUTH_CONFIG 节点的账号信息为您的账号密码。
 *      3. 首次使用请修改 $config[ENVIRONMENTS] 节点的数据库服务器连接信息。
 *      4. 从浏览器打开 http://yoursitedomain/mysqladminlite.php 开始使用。
 *      5. 命令行使用：（命令行初始化后会生成 mysal 目录，请修改 mysal/config/config.php 中的数据库连接配置）
 *          php mysqladminlite.php --help #显示帮助内容
 *          php mysqladminlite.php --init #初始化
 */
// 检测依赖扩展
if (!mysal_check_extension('PDO')) {
    exit($name . '扩展未开启');
}
// 版本号
define('VERSION', '5.0.5');
// 程序名称
define('APPNAME', 'mysqladminlite');

// 重要关键词常量定义
const DS = DIRECTORY_SEPARATOR;
const ENVIRONMENTS = 'environments';
const DEFAULT_DATABASE = 'default_database';
const FAVORITE = 'favorite';
const MIGRATION_FRAMEWORK = 'migration_framework';
const FASTSQLS = 'fastsqls';
const RINETD = 'rinetd';
const EXTFILE = 'extfile';
const UI_RESOURCE = 'ui_resource';
// 全局配置
$config = [];
// 登录账号配置
$config['auth'] = [
    // 允许登录的帐号列表 ['登录名'=>'密码']
    'admins' => ['myadmin' => 'passWd123', 'mydbm' => 'passWd123'],
    // 允许访问的 ip 地址列表
    'ips' => ['127.0.0.1', '110.52.27.168'],
    // 指定页面访问验证类型：0=无需验证;1=帐号密码验证;2=IP验证;3=混合验证（先IP验证，IP验证失败则转入帐号密码验证）
    'current_authtype' => 1,
    // ===================
    // TOKEN 设置
    // ===================
    // TOKEN_EXP 超时时间 (1 hours, 1 minutes, 30 seconds)
    'token_exp' => '1 hours',
    // TOKEN_AUD 允许运行的服务器列表(适用于分布式环境，如果是负载均衡+高防IP环境，则填入高防IP即可)
    // 多个之间以逗号隔开，中间不要有空格，例如：127.0.0.1,110.52.27.168
    'token_auth' => '127.0.0.1,110.52.27.168',
    // TOKEN 签名密钥
    'token_key' => 'U6rxInZ7!',
    // TOKEN 自动延期时长(秒)，每次页面跳转操作会重新生成一个延期过的 TOKEN，类似 session 的效果，只要一直操作就一直不会过期
    'token_referesh_second' => 1800,
];
// 数据库服务器环境配置
$config['environments'] = [
    // 指定默认环境
    'default_database' => 'development',
    // 开发环境
    'development' => [
        'type' => 'mysql',
        'host' => 'localhost',
        'user' => 'root',
        'password' => '',
        'database' => 'dbexample',
        'port' => 3306,
        'charset' => 'utf8',
        'prefix' => '', // 表名前缀，用于侧边栏过滤表名前缀
        'isproduction' => false, // 是否正式生产环境，用于在界面上警示提醒
        // 外部配置文件，path 为路径, map 为字段映射，外部配置会覆盖内部配置，startline 为文本配置文件的有效起始行内容
        'extfile' => [
            'enable' => false,
            'path' => '../.env',
            'map' => [
                'startline' => '[database]',
                'host' => 'hostname', 'user' => 'username', 'password' => 'password', 'port' => 'hostport',
                'database' => 'database', 'prefix' => 'prefix'
            ]
        ],
    ],
    // 测试环境
    'testing' => [],
    // 生产环境
    'production' => [
        'type' => 'mysql',
        'host' => '',
        'user' => '',
        'password' => '',
        'database' => '',
        'port' => 3306,
        'charset' => 'utf8',
        'prefix' => 't_', // 表名前缀，用于侧边栏过滤表名前缀
        'isproduction' => true, // 是否正式生产环境，用于在界面上警示提醒
        // 端口映射服务器地址，如果配置该字段，则会解析时会替换 host 和 port 字段的值
        'rinetd' => ['host' => '30.60.100.16', 'port' => 3366],
        // 外部配置文件，path 为路径, map 为字段映射，外部配置会覆盖内部配置，startline 为文本配置文件的有效起始行内容
        'extfile' => [],
    ],
    // 基于 laravel 框架的第三方应用程序环境（vendorapp）
    // 比如安装某个网站系统会自动生成相关的数据库配置文件，通过指定配置文件路径和字段映射，可以方便读取网站应用的配置文件
    'laravelapp' => [
        // laravel 项目的 .env 文件中没有 charset 配置项，因此这里要补充该配置项
        'charset' => 'utf8mb4',
        'extfile' => [
            'path' => '../.env',
            // map 为字段映射，外部配置会覆盖内部配置，startline 为文本配置文件的有效起始行内容
            'map' => [
                'startline' => 'DB_CONNECTION=mysql',
                'host' => 'DB_HOST', 'user' => 'DB_USERNAME', 'password' => 'DB_PASSWORD', 'port' => 'DB_PORT',
                'database' => 'DB_DATABASE',
            ]
        ],
    ],
    // 基于 thinkphp 框架的第三方应用程序环境（vendorapp）
    'thinkphpapp' => [
        // thinkphp 项目的 .env 文件中没有 charset 配置项，因此这里要补充该配置项
        'charset' => 'utf8',
        'extfile' => [
            'enable' => false,
            'path' => './mysal/config/thinkphp.env',
            // map 为字段映射，外部配置会覆盖内部配置，startline 为文本配置文件的有效起始行内容
            'map' => [
                'startline' => '[database]',
                'host' => 'hostname', 'user' => 'username', 'password' => 'password', 'port' => 'hostport',
                'database' => 'database', 'prefix' => 'prefix'
            ]
        ],
    ]
];
// 数据迁移框架（目前支持 phinx 和 mysal[mysqladminlite] 两种）
$config['migration_framework'] = 'phinx';
// UI 静态资源配置
$config['ui_resource'] = [
    'provider' => 'cdn',
    'css' => [
        '_tpl' => '<link href="%s" rel="stylesheet" />',
        'bootstrap' => [
            'cdn' => 'https://cdn.bootcss.com/twitter-bootstrap/4.4.1/css/bootstrap.min.css',
            'local' => '/assets/dist/css/adminlte.min.css'
        ]
    ],
    'javascript' => [
        '_tpl' => '<script src="%s"></script>',
        'jquery' => [
            'cdn' => 'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js',
            'local' => '/assets/dist/plugins/jquery/jquery.min.js'
        ],
        'bootstrap' => [
            'cdn' => 'https://cdn.bootcss.com/twitter-bootstrap/4.4.1/js/bootstrap.bundle.min.js',
            'local' => '/assets/dist/plugins/bootstrap/bootstrap.bundle.min.js'
        ]
    ]
];
// 教程网址收藏夹
$config['tutorial'] = [
    '教程手册' => [
        ['title' => 'MySQL 教程 | 菜鸟教程', 'url' => 'https://www.runoob.com/mysql/mysql-tutorial.html'],
        ['title' => 'MySQL 教程_w3cschool', 'url' => 'https://www.w3cschool.cn/mysql/'],
        ['title' => 'Mysql中时间和日期函数', 'url' => 'https://www.iteblog.com/mysql中时间和日期函数/'],
        ['title' => 'MySQL：日期函数、时间函数总结', 'url' => 'https://www.cnblogs.com/ggjucheng/p/3352280.html'],
        ['title' => 'mysql时间日期函数', 'url' => 'https://www.cnblogs.com/weibanggang/p/9574987.html'],
    ],
];
// 数据表收藏(在 侧边栏-快捷跳转 数据表下拉框中置顶显示)
$config['favorite'] = [
    'dbexample' => ['table1', 'table2'],
    'dbtest' => ['tbl_user', 'ps_admin'],
];
// 快速查询：数据表的一些常用查询语句可以配置到快捷查询中(name 用英文，remark 描述务必填写)
// 支持 {#object} 变量, 如: ['tbl_log_'.date('Ym') => ['simple' => ['sql' => 'select * from {#object} limit 10']]]
// 格式：['table1'=>['name1'=>['sql'=>'','remark'=>''],'name1'=>['sql'=>'','remark'=>'']]
$config['fastsqls'] = [
    // 用户表
    'tbl_user' => [
        'all' => [
            'text' => '全部查',
            'remark' => '查询全部字段',
            'sql' => 'select * from {#object} limit 100;'
        ],
        'simple' => [
            'text' => '简单查',
            'remark' => '较少字段查询',
            'sql' => 'select id, username, nickname, mobile, level, gender, money, score, status, '
                . 'from_unixtime(`createtime`+28800) from {#object} where 1=1 order by id desc limit 30;'
        ],
    ],
    // 日志月份分表
    'tbl_log_' . date('Ym') => [
        'simple' => [
            'text' => '简单查',
            'remark' => '查询较少字段',
            'sql' => 'select uid, createtime, updatetime from {#object}'
                . ' order by addtime desc limit 50;'
        ],
        'export' => [
            'text' => '查询导出',
            'remark' => '导出查询记录',
            'sql' => "select `uid` as '用户ID', `createtime` as '接取时间', `updatetime` as '完成时间' from {#object}"
                . " order by createtime desc",
        ],
    ],
    // 全局群组配置，用于按月份创建的数据表。
    // 键值数组中包含的所有的日期分表都会自动设置成和键名指向的数据表的配置内容一样的内容
    '-group-' => [
        'ps_task_log_' . date('Ym') => ['ps_task_log_201912', 'ps_task_log_20203'],
    ]
];

// 如果外部配置文件存在，则外部配置文件会覆盖内置配置文件
// 外部配置文件可以通过命令行：php mysqladminlite.php init 自动生成
ConfigHelper::loadExtConfig($config, ConsoleHelper::MYSAL_CONFIG_PATH . 'config.php');

// 解析环境中的特殊配置，如第三方app外部环境和端口转发配置，解析后返回规范化的配置内容
$config[ENVIRONMENTS] = ConfigHelper::parseEnvironments($config[ENVIRONMENTS]);

// 命令行模式，在命令行输入命令"php mysqladminlite.php --help"查看命令帮助
if (php_sapi_name() == 'cli') {
    $clihelper = new ConsoleHelper();
    $clihelper->run($argc, $argv, $config);
    exit();
}

// UI 设置
HtmlHelper::setUiResource($config[UI_RESOURCE]);
// 页面访问验证配置
JWTLite::init($config);


// 浏览器参数处理
$curenv = HtmlHelper::input('env', $config[ENVIRONMENTS][DEFAULT_DATABASE]); // 当前服务器ID
$envs = HtmlHelper::input('envs'); // 批量执行的分布式服务器编号列表
$curdbname = HtmlHelper::input('db'); // 当前数据库名称
$curtable = HtmlHelper::input('table'); // 当前表名称
$curview = HtmlHelper::input('view'); // 当前视图名称
$curroutine = HtmlHelper::input('routine'); // 当前存储程序名称
$curroutinetype = HtmlHelper::input('routinetype'); // 当前存储程序类型
$sqlstmt = HtmlHelper::input('sqlstmt', '', false); // 当前 sql 语句
$action = HtmlHelper::input('action'); // 当前动作
$curobject = $curtable | $curview | $curroutine; // 当前对象名称，用于标题提示等
$curobjecttype = ''; // 当前对象类型，包括 table,view,procedure,function 四种类型

if ($curtable) {
    $curobjecttype = 'table';
}
if ($curview) {
    $curobjecttype = 'view';
}
if ($curroutine && $curroutinetype) {
    $curobjecttype = $curroutinetype;
}
// 尝试从 sql 中解析出 object 对象
if (!$curobject && $sqlstmt) {
    $sqlstmtObject = PdoConnector::getObjectBySqlscript($sqlstmt);
    if ($sqlstmtObject['object']) {
        $curobject = $sqlstmtObject['object'];
        if ($sqlstmtObject['type'] == 'table') {
            $curtable = $curobject;
        } else {
            $curroutine = $curobject;
            $curroutinetype = $sqlstmtObject['type'];
        }
    }
}
// 显示 phpinfo 信息
if ('phpinfo' == strtolower($action)) {
    phpinfo();
    exit();
}
// 是否精简模式，精简模式用于 iframe 引用,示例：mysqladminlite.php?litemode=1&sqlstmt=...
$litemode = intval(HtmlHelper::input('litemode'));
$loginUrlParams = [];
if ($litemode) {
    $loginUrlParams['litemode'] = $litemode;
}
if ($sqlstmt) {
    $loginUrlParams['sqlstmt'] = $sqlstmt;
}
// 页面访问验证配置
JWTLite::checkLogin($loginUrlParams);
list($loginid, $token, $loginexp) = array_values(JWTLite::$LOGIN_RESULT);

// 文件处理
FileHelper::saveFileHandler();
FileHelper::readFileHandler();

// 数据库配置解析
$server = ConfigHelper::getCurrentEnv($config, $curenv, false);

// 解析 host 和 port
$host = $server['host'];
$port = $server['port'];
$isprod = isset($server['isproduction']) && $server['isproduction']; // 是否正式生产环境
$titleprefix = $isprod ? "[正式服$curenv] " : "[$curenv] "; // 页面标题前缀，显示当前服务器
$tableprefix = isset($server['prefix']) ? $server['prefix'] : ''; // 表名前缀
if (!$curdbname && !empty($server['database'])) {
    $curdbname = $server['database']; // 当前数据库名称
}
if ($curdbname) {
    $server['database'] = $curdbname; // 指定数据库
}
// 实例化数据库操作类
try {
    $pdoadapter = new PdoConnector($server);
} catch (\Exception $ex) {
    echo $ex->getMessage();
    exit;
}
// 导出数据库表结构到 Markdown|Excel|HTML 文件
if ($curdbname && $action) {
    switch ($action) {
        case 'dbexportasmarkdown':
            $pdoadapter->exportDbSchemaAsMarkdown($curdbname);
            break;
        case 'dbexportasexcel':
            $pdoadapter->exportDbSchemaAsExcelForDownload($curdbname);
            break;
        case 'dbexportashtml':
            $pdoadapter->exportDbSchemaAsHtmlForDownload($curdbname);
            break;
        case 'dbmodel':
            $pdoadapter->exportDbSchemaAsHtmlOnline($curdbname);
            break;
        case 'dbexportassql':
            $pdoadapter->exportDbSchemaAsSqlForDownload($curdbname);
            break;
    }
    if ($curtable) {
        switch ($action) {
            case 'exportasmigrate':
                $pdoadapter->exportTableSchemaAsMigrateForDownload($curdbname, $curtable, $config[MIGRATION_FRAMEWORK]);
                break;
            case 'exportasseed':
                $pdoadapter->exportTableSchemaAsSeedForDownload($curdbname, $curtable, $config[MIGRATION_FRAMEWORK]);
                break;
            case 'tableexportashtml':
                $pdoadapter->exportTableSchemaAsHtmlOnline($curdbname, $curtable);
                break;
            case 'tableexportasexcel':
                $pdoadapter->exportTableSchemaAsExcelForDownload($curdbname, $curtable);
                break;
            case 'tableexportassql':
                $pdoadapter->exportDbSchemaAsSqlForDownload($curdbname, $curtable);
                break;
        }
    }
}
// 导出查询数据到 Markdown 文件
if ($sqlstmt && $action) {
    switch ($action) {
        case 'exportmd':
            $pdoadapter->exportQueryDataAsMarkdownForDownload($curdbname, $curtable, $sqlstmt);
            break;
        case 'exportexcel':
            $pdoadapter->exportQueryDataAsExcelForDownload($curdbname, $curtable, $sqlstmt);
            break;
        case 'exporthtml':
            $pdoadapter->exportQueryDataAsHtmlForDownload($curdbname, $curtable, $sqlstmt);
            break;
        case 'exporthtmlonline':
            $pdoadapter->exportQueryDataAsHtmlOnline($curdbname, $curtable, $sqlstmt);
            break;
        case 'exportjson':
            $pdoadapter->exportQueryDataAsJsonForDownload($curdbname, $curtable, $sqlstmt);
            break;
        case 'exportjsononline':
            $pdoadapter->exportQueryDataAsJsonForWeb($curdbname, $curtable, $sqlstmt);
            break;
        case 'exportsql':
            // 导出查询数据到 SQL 文件
            if ($curtable && $curdbname) {
                $pdoadapter->exportQueryDataAsSqlForDownload($curdbname, $curtable, $sqlstmt);
            }
            break;
    }
}
// 全局变量

// 配置中的环境配置列表
$environmentList = $config[ENVIRONMENTS];
// 当前环境的数据库服务器中的数据库列表
$databaseList = $pdoadapter->getDatabases();
// 当前数据库所有数据表的结构信息
$tableList = [];
// 当前数据库所有视图的结构信息
$viewList = [];
// 当前数据库所有函数的结构信息
$functionList = [];
// 当前数据库所有存储过程的结构信息
$procedureList = [];
?>
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title><?php echo $titleprefix, ($curobject ? $curobject . ' - ' : ''), APPNAME, ' - ', VERSION ?></title>
    <?php echo HtmlHelper::getResourceLink('bootstrap') ?>
    <style>
        body {
            font-size: 13px;
        }

        .nav-tabs {
            margin-top: 5px;
        }

        .tab-pane {
            padding-top: 6px;
            max-height: 250px;
        }

        .nav-tabs a.nav-link {
            padding: 0.3rem 1rem;
        }

        .overview {
            line-height: 25px;
        }

        .overview mark {
            margin-right: 1em;
        }

        .table td,
        .table th {
            font-size: 12px;
        }

        aside .list-unstyled {
            line-height: 21px
        }

        #nav-table {
            max-height: 400px
        }

        .btngroup a {
            margin-right: 5px
        }

        .accordion .card-header {
            padding: 0
        }

        .accordion .card-header h2 button {
            font-size: 13px
        }

        .accordion .card-body {
            padding: 0.5rem
        }

        .code-editor {
            background-color: #384548 !important;
            color: #abe338 !important;
        }
    </style>
</head>

<body>
    <?php
    // 精简模式 2017-12-15
    if ($litemode == 1) {
        echo '<form action="?action=run">';
        echo '<input type="text" name="sqlstmt" style="width:100%" value="', $sqlstmt, '" />';
        echo '<button type="submit">执行</button> <button type="reset">重置</button>';
        echo '<input type="hidden" name="env" value="', $curenv, '" />';
        echo '<input type="hidden" name="db" value="', $curdbname, '" />';
        echo '<input type="hidden" name="table" value="', $curtable, '" />';
        echo '<input type="hidden" name="token" value="', $token, '" />';
        echo '<input type="hidden" name="litemode" value="1" />', '</form>';
        if ($sqlstmt) {
            if (0 === strpos($sqlstmt, 'select ')) {
                try {
                    $list = $pdoadapter->query($sqlstmt);
                    HtmlHelper::showTableWebLite($list);
                    unset($list);
                } catch (\Exception $ex) {
                    echo $ex->getMessage();
                }
            } else {
                echo '命令不支持：', $sqlstmt;
            }
        }
        exit;
    }
    // 完整模式
    echo HtmlHelper::getResourceLink(['jquery', 'bootstrap'], 'javascript');
    ?>
    <div class="container-fluid">
        <nav class="navbar navbar-expand-lg navbar-light bg-light">
            <a class="navbar-brand" href="#"><?php echo APPNAME ?></a>
            <span class="badge badge-info"><?php echo VERSION ?></span>
            <div class="collapse navbar-collapse" id="navbarText">
                <ul class="navbar-nav mr-auto">
                    <?php
                    $eltpl = "<li class='nav-item'><a class='nav-link' href='%2\$s' title='%3\$s' %4\$s>%1\$s</a></li>";
                    echo sprintf($eltpl, '新窗口', '?env=' . $curenv . '&token=' . $token, '新开一个页面', 'target="_blank"');
                    echo sprintf($eltpl, '重置', '?env=' . $curenv . '&token=' . $token, '重置当前页面', '');
                    echo sprintf($eltpl, '刷新', '#', '刷新当前页面', 'onclick="location.reload();"');
                    echo sprintf($eltpl, 'phpinfo', '?action=phpinfo', '查看 phpinfo() 信息', 'target="_blank"');
                    ?>
                </ul>
                <div class="form-inline">
                    <div>
                        <?php
                        // 用户登录信息
                        echo '<table><tr><td>';
                        echo '<span title="登录过期时间：', $loginexp;
                        echo ' (超时前进行跳转操作能让登录会话自动延期,避免重登录操作~)">', $loginid, '</span> ';
                        echo '<a class="btn btn-outline-secondary btn-sm" href="?action=logout" ',
                            'onclick="return confirm(\'您确定要注销登录吗?\');">注销</a> ';
                        // 显示本地时间
                        echo '</td><td>';
                        HtmlHelper::showLocaltime('<span class="align-items-center" style="display:inline-block;'
                            . 'font-size:12px;margin-left:3px" title="第 {{yearweek}} 周 / 共 {{yearweeks}} 周，'
                            . '第 {{yearday}} 天 / 共 {{yeardays}} 天！今年剩余 {{remaindays}} 天，{{remainweeks}} 周！' . PHP_EOL
                            . '{{motto}}">' . date('Y-m-d') . '<br />' . date('H:i') . ' 星期{{weekday}}</span>');
                        echo '</td></tr></table>';
                        ?>
                    </div>
                </div>
        </nav>
        <!--数据库信息-->
        <nav>
            <div class="nav nav-tabs">
                <a class="nav-item nav-link active" id="nav-dbinfo-empty-tab" data-toggle="tab" href="#nav-dbinfo-empty" title="Overview">概览</a>
                <a class="nav-item nav-link" id="nav-dbinfo-tab" data-toggle="tab" href="#nav-dbinfo" title="Status Variables">数据库状态变量</a>
                <a class="nav-item nav-link" id="nav-optimizes-tab" data-toggle="tab" href="#nav-optimizes" title="Optimizes Advisor">优化建议</a>
                <a class="nav-item nav-link" id="nav-settings-tab" data-toggle="tab" href="#nav-settings" title="Settings">设置</a>
                <a class="nav-item nav-link" id="nav-tutorial-tab" data-toggle="tab" href="#nav-tutorial" title="Tutorial">教程</a>
            </div>
        </nav>
        <div class="tab-content" id="nav-tabContent">
            <div class="tab-pane fade overflow-auto show active" id="nav-dbinfo-empty">
                <?php
                echo '<p class="overview">客户端IP <mark>', $_SERVER['REMOTE_ADDR'],
                    '</mark><span style="display:none">服务器IP <mark>';
                echo JWTLite::getHostIP(), '</mark>数据库连接 <mark title="', $host, ':', $port, '">', $host,
                    '</mark></span> ';
                echo '<a href="#" id="lnk-toggle-ipinfo">[...]</a><br />环境 <mark>', $curenv, '</mark>数据库 ';
                echo '<mark>', $curdbname, '</mark>MySQL版本 <mark>', $pdoadapter->getDbVersion(), '</mark></p>';
                ?>
            </div>
            <div class="tab-pane fade overflow-auto" id="nav-dbinfo">
                <?php
                $optimizes = [];
                HtmlHelper::showDbVariablesTable($pdoadapter->getDbVariables(), $optimizes);
                ?>
            </div>
            <div class="tab-pane fade overflow-auto" id="nav-optimizes">
                <?php
                if ($optimizes) {
                    echo '<h6>数据库配置文件优化建议</h6>';
                    HtmlHelper::startTable();
                    echo '<tr><th>变量名</th><th>当前值</th><th>建议值</th><th>描述</th></tr>';
                    foreach ($optimizes as $row) {
                        echo '<tr><td>', $row['name'], '</td><td>', $row['value'], '</td><td>', $row['recommend'];
                        echo '</td><td>', $row['remark'], '</td></tr>';
                    }
                    echo '</table>';
                }
                ?>
            </div>
            <div class="tab-pane fade" id="nav-settings">...</div>
            <div class="tab-pane fade" id="nav-tutorial">
                <?php
                if (!empty($config['tutorial'])) {
                    $tutorials = $config['tutorial'];
                    $groupCount = count($tutorials);
                    $colCount = floor(12 / $groupCount);
                    echo '<div class="row">';
                    foreach ($tutorials as $groupTitle => $rows) {
                        echo '<div class="col-', $colCount, '"><div class="card"><div class="card-body">';
                        echo '<h5 class="card-title">', $groupTitle,
                            '</h5><ul class="list-unstyled overflow-auto" style="max-height:150px">';
                        foreach ($rows as $row) {
                            echo '<li><a href="', $row['url'], '" target="_blank" title="', $row['title'], '">',
                                mysal_short_text($row['title'], 30),
                                '</a></li>';
                        }
                        echo '</ul></div></div></div>';
                    }
                    echo '</div>';
                }
                ?>
            </div>
        </div>
        <!--/数据库信息-->
        <!--侧边栏-->
        <div class="row">
            <aside class="col-md-2 d-none d-md-block bg-light sidebar">
                <!--数据库选择器-->
                <nav>
                    <div class="nav nav-tabs">
                        <a class="nav-item nav-link" id="nav-dbpicker-empty-tab" data-toggle="tab" href="#nav-dbpicker-empty" title="折叠此面板">&equiv;</a>
                        <a class="nav-item nav-link active" id="nav-db-tab" data-toggle="tab" href="#nav-db">数据库</a>
                        <a class="nav-item nav-link" id="nav-env-tab" data-toggle="tab" href="#nav-env">环境</a>
                    </div>
                </nav>
                <div class="tab-content" id="nav-tabContent">
                    <div class="tab-pane fade overflow-auto" id="nav-dbpicker-empty"></div>
                    <div class="tab-pane fade overflow-auto show active" id="nav-db">
                        <?php
                        /* 侧边栏显示对象名称列表 */
                        // SCHEMA_NAME,DEFAULT_CHARACTER_SET_NAME,DEFAULT_COLLATION_NAME
                        echo '<select class="form-control form-control-sm" id="dbslt">';
                        echo '<option value="">==选择数据库==</option>';
                        foreach ($databaseList as $v) {
                            $objname = $v['SCHEMA_NAME'];
                            echo '<option value="', $objname, '"';
                            echo ($curdbname == $objname ? ' selected="selected"' : '');
                            echo ' data-url="?env=', $curenv, '&db=', $objname, '&token=', $token, '" title="';
                            echo $objname,                                '">';
                            echo mysal_short_text($objname, 12), ' [', $v['DEFAULT_CHARACTER_SET_NAME'], ']';
                            echo '</option>';
                        }
                        echo '</select>';
                        ?>
                    </div>
                    <div class="tab-pane fade overflow-auto" id="nav-env">
                        <?php
                        echo '<select class="form-control form-control-sm" id="servslt">';
                        $tmpisprod = false;
                        foreach ($environmentList as $k => $v) {
                            // 过滤无效环境配置节点
                            if (DEFAULT_DATABASE == $k || empty($v) || empty($v['password'])) {
                                continue;
                            }
                            $tmpisprod = isset($v['isproduction']) && $v['isproduction'];
                            echo '<option value="', $k, '"', ($curenv == $k ? ' selected' : '');
                            echo ' data-url="?env=', $k, '&token=', $token, '"';
                            echo ' title="', $k, ($tmpisprod ? '(生产环境)' : ''), '"',
                                ($tmpisprod ? ' style="color:#F00"' : ''),
                                '>';
                            echo mysal_short_text($k, 12);
                            echo '</option>';
                        }
                        echo '</select>';
                        ?>
                    </div>
                </div>
                <!--/数据库选择器-->
                <!--转到-->
                <nav>
                    <div class="nav nav-tabs">
                        <a class="nav-item nav-link" id="nav-jump-empty-tab" data-toggle="tab" href="#nav-jump-empty" title="折叠此面板">&equiv;</a>
                        <a class="nav-item nav-link active" id="nav-jump-tab" data-toggle="tab" href="#nav-jump">转到</a>
                        <a class="nav-item nav-link" id="nav-export-tab" data-toggle="tab" href="#nav-export">导出</a>
                    </div>
                </nav>
                <div class="tab-content" id="nav-tabContent">
                    <div class="tab-pane fade overflow-auto" id="nav-jump-empty"></div>
                    <div class="tab-pane fade overflow-auto show active" id="nav-jump">
                        <form action="?action=jump">
                            跳转到：
                            <input class="form-control form-control-sm" type="text" name="table" value="<?php echo $curobject ? $curobject : $tableprefix ?>" title="<?php echo $curobject ?>" required="required" placeholder="数据表/视图/函数" />
                            <div style="margin-top:5px">
                                <button class="btn btn-primary btn-sm" type="submit" title="跳转到表" data-db="<?php echo $curdbname ?>">确定</button>
                                <button class="btn btn-secondary btn-sm" type="reset">重置</button>
                            </div>
                            <input type="hidden" name="action" value="egselect" />
                            <input type="hidden" name="env" value="<?php echo $curenv ?>" />
                            <input type="hidden" name="db" value="<?php echo $curdbname ?>" />
                            <input type="hidden" name="token" value="<?php echo $token ?>" />
                        </form>
                        <?php
                        if ($curdbname) {
                            // TABLE_NAME, TABLE_TYPE, ENGINE, DATA_LENGTH, CREATE_TIME, TABLE_COLLATION, TABLE_COMMENT
                            $tableList = $pdoadapter->getTables($curdbname);
                            $viewList = $pdoadapter->getViews($curdbname); // TABLE_NAME
                            // ROUTINE_NAME, ROUTINE_TYPE
                            $procedureList = $pdoadapter->getRoutines($curdbname, 'procedure');
                            // ROUTINE_NAME, ROUTINE_TYPE
                            $functionList = $pdoadapter->getRoutines($curdbname, 'function');
                            if ($tableList || $viewList || $procedureList || $functionList) {
                                echo '<div style="margin-top:5px;">快捷跳转：';
                                echo '<div style="line-height:24px;">';
                                HtmlHelper::showAsideTableList($tableList, $tableprefix, false);
                                HtmlHelper::showAsideViewList($viewList, false);
                                HtmlHelper::showAsideRoutineList('procedure', $procedureList, false);
                                HtmlHelper::showAsideRoutineList('function', $functionList, false);
                                echo '</div></div>';
                            }
                        }
                        ?>
                    </div>
                    <div class="tab-pane fade overflow-auto" id="nav-export">
                        <?php
                        if ($curdbname) {
                            HtmlHelper::showAsideDbOptionButtons();
                        }
                        ?>
                    </div>
                </div>
                <!--/转到-->
                <!--数据表-->
                <nav>
                    <div class="nav nav-tabs">
                        <a class="nav-item nav-link" id="nav-jump-table-tab" data-toggle="tab" href="#nav-table-empty" title="折叠此面板">&equiv;</a>
                        <a class="nav-item nav-link active" id="nav-table-tab" data-toggle="tab" href="#nav-table">数据表</a>
                        <a class="nav-item nav-link" id="nav-view-tab" data-toggle="tab" href="#nav-view">视图</a>
                    </div>
                </nav>
                <div class="tab-content" id="nav-tabContent">
                    <div class="tab-pane fade overflow-auto" id="nav-table-empty"></div>
                    <div class="tab-pane fade overflow-auto show active" id="nav-table">
                        <?php
                        if ($curdbname && $tableList) {
                            HtmlHelper::showAsideTableList($tableList, $tableprefix);
                        }
                        ?>
                    </div>
                    <div class="tab-pane fade overflow-auto" id="nav-view">
                        <?php
                        if ($curdbname && $viewList) {
                            HtmlHelper::showAsideViewList($viewList);
                        }
                        ?>
                    </div>
                </div>
                <!--/数据表-->

                <!--存储过程-->
                <nav>
                    <div class="nav nav-tabs">
                        <a class="nav-item nav-link" id="nav-routine-table-tab" data-toggle="tab" href="#nav-routine-empty" title="折叠此面板">&equiv;</a>
                        <a class="nav-item nav-link active" id="nav-procedure-tab" data-toggle="tab" href="#nav-procedure">过程</a>
                        <a class="nav-item nav-link" id="nav-function-tab" data-toggle="tab" href="#nav-function">函数</a>
                    </div>
                </nav>
                <div class="tab-content" id="nav-tabContent">
                    <div class="tab-pane fade overflow-auto" id="nav-routine-empty"></div>
                    <div class="tab-pane fade overflow-auto show active" id="nav-procedure">
                        <?php
                        if ($curdbname && $procedureList) {
                            HtmlHelper::showAsideRoutineList('procedure', $procedureList);
                        }
                        ?>
                    </div>
                    <div class="tab-pane fade overflow-auto" id="nav-function">
                        <?php
                        if ($curdbname && $functionList) {
                            HtmlHelper::showAsideRoutineList('function', $functionList);
                        }
                        ?>
                    </div>
                </div>
                <!--/存储过程-->
            </aside>
            <main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-4">
                <?php
                $objtitle = $curdbname . ' 数据库';
                $objunfind = false;
                if ($curdbname && $curobjecttype) {
                    switch ($curobjecttype) {
                            // 当前数据表对象的操作
                        case 'table':
                            $objtitle = $curobject . ' 数据表';
                            // COLUMN_NAME, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION,
                            // NUMERIC_SCALE, EXTRA, COLUMN_COMMENT
                            $columnList = $pdoadapter->getColumns($curdbname, $curtable);
                            $objunfind = empty($columnList);
                            $fields = mysal_array_column($columnList, 'COLUMN_NAME'); // 字段名称集合
                            // 获取具体示例语句
                            if ($action) {
                                $sqlstmt = SqlHelper::getTableExampleSql($action, $curdbname, $curtable, $fields);
                            }
                            break;
                            // 当前视图对象的操作
                        case 'view':
                            $objtitle = $curobject . ' 视图';
                            // COLUMN_NAME, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION,
                            // NUMERIC_SCALE, EXTRA, COLUMN_COMMENT
                            $columnList = $pdoadapter->getColumns($curdbname, $curview);
                            $objunfind = empty($columnList);
                            $fields = mysal_array_column($columnList, 'COLUMN_NAME'); // 字段名称集合// 获取示例语句
                            // 获取具体示例语句
                            if ($action) {
                                $sqlstmt = SqlHelper::getViewExampleSql($action, $curdbname, $curview, $fields);
                            }
                            break;
                            // 当前程序对象的操作
                        case 'procedure':
                        case 'function':
                            $objtitle = $curjboect . ($curroutinetype == 'procedure' ? ' 过程' : ' 函数');
                            // 参数名称集合 ORDINAL_POSITION, PARAMETER_MODE, PARAMETER_NAME, DATA_TYPE,
                            // CHARACTER_MAXIMUM_LENGTH, DTD_IDENTIFIER
                            $routineParamList = $pdoadapter->getRoutineParams($curdbname, $curroutine);
                            $routineParamNames = mysal_array_column($routineParamList, 'PARAMETER_NAME');
                            $pdoadapter->getRoutineParamsSimple($curdbname, $curroutine); // 获取示例语句
                            // 获取具体示例语句
                            if ($action) {
                                $sqlstmt = SqlHelper::getRoutineExampleSql(
                                    $action,
                                    $curroutine,
                                    $curroutinetype,
                                    $routineParamNames
                                );
                            }
                            break;
                    }
                }
                ?>
                <!--数据表信息-->
                <nav>
                    <div class="nav nav-tabs">
                        <a class="nav-item nav-link" id="nav-routine-tableinfo-tab" data-toggle="tab" href="#nav-tableinfo-empty" title="折叠此面板">&equiv;</a>
                        <a class="nav-item nav-link active" id="nav-tableinfo-tab" data-toggle="tab" href="#nav-tableinfo"><?php echo $objtitle ?></a>
                    </div>
                </nav>
                <div class="tab-content" id="nav-tabContent">
                    <div class="tab-pane fade overflow-auto" id="nav-tableinfo-empty"></div>
                    <div class="tab-pane fade overflow-auto show active" id="nav-tableinfo">
                        <!--accordion-->
                        <div class="accordion" id="accordionTableinfo">
                            <div class="card">
                                <div class="card-header" id="headingOneTableinfo">
                                    <h2 class="mb-0">
                                        <button class="btn btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseOneTableinfo" aria-expanded="false" aria-controls="collapseOneTableinfo">
                                            操作命令示例
                                        </button>
                                    </h2>
                                </div>
                                <div id="collapseOneTableinfo" class="collapse show" aria-labelledby="headingOneTableinfo" data-parent="#accordionTableinfo">
                                    <div class="card-body">
                                        <?php
                                        if (!$objunfind) {
                                            switch ($curobjecttype) {
                                                case 'table':
                                                    HtmlHelper::showTableOptionButtons();
                                                    break;
                                                case 'view':
                                                    HtmlHelper::showViewOptionButtons($fields);
                                                    break;
                                                case 'function':
                                                case 'procedure':
                                                    HtmlHelper::showRoutineOptionButtons($routineParamNames);
                                                    break;
                                                default:
                                                    HtmlHelper::showDefaultOptionButtons();
                                                    break;
                                            }
                                        }
                                        ?>
                                    </div>
                                </div>
                            </div>
                            <div class="card">
                                <div class="card-header" id="headingTwoTableinfo">
                                    <h2 class="mb-0">
                                        <button class="btn btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseTwoTableinfo" aria-expanded="true" aria-controls="collapseTwoTableinfo">
                                            对象信息
                                        </button>
                                    </h2>
                                </div>
                                <div id="collapseTwoTableinfo" class="collapse show" aria-labelledby="headingTwoTableinfo" data-parent="#accordionTableinfo">
                                    <div class="card-body">
                                        <?php
                                        if ($objunfind) {
                                            echo '<div class="text-danger">' . $objtitle
                                                . '不存在! <a class="btn btn-sm btn-secondary" href="?env=' . $curenv
                                                . '&token=' . $token . '">刷新</a></span>';
                                        } else {
                                            switch ($curobjecttype) {
                                                case 'table':
                                                    echo '<div>字段概览</div>';
                                                    echo '<code>', implode(', ', $fields), '</code>';
                                                    echo '<div>数据表信息</div>';
                                                    HtmlHelper::showTableWebLite(mysal_array_search_rows($tableList, ['TABLE_NAME' => $curtable]));
                                                    echo '<div>数据列信息</div>';
                                                    HtmlHelper::showTableWeb($columnList);
                                                    break;
                                                case 'view':
                                                    echo '<code>', implode(', ', $fields), '</code>';
                                                    echo '<div>数据列信息</div>';
                                                    HtmlHelper::showTableWeb($columnList);
                                                    break;
                                                case 'procedure':
                                                case 'function':
                                                    HtmlHelper::showPropertyTable($routineParamList);
                                                    echo implode(', ', $routineParamNames);
                                                    break;
                                                default:
                                                    echo '<div>数据库信息</div>';
                                                    HtmlHelper::showTableWebLite(mysal_array_search_rows($databaseList, ['SCHEMA_NAME' => $curdbname]));
                                                    echo '<div>数据表信息</div>';
                                                    HtmlHelper::showTableWebLite($tableList);
                                                    break;
                                            }
                                        }
                                        ?>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <!--/accordion -->
                    </div>
                </div>
                <!--/数据表信息-->

                <!--SQL编辑器-->
                <nav>
                    <div class="nav nav-tabs">
                        <a class="nav-item nav-link" id="nav-editor-empty-tab" data-toggle="tab" href="#nav-editor-empty" title="折叠此面板">&equiv;</a>
                        <a class="nav-item nav-link active" id="nav-sqleditor-tab" data-toggle="tab" href="#nav-sqleditor">SQL 编辑器</a>
                        <a class="nav-item nav-link" id="nav-mysaleditor-tab" data-toggle="tab" href="#nav-mysaleditor">文件编辑器</a>
                    </div>
                </nav>
                <div class="tab-content" id="nav-tabContent">
                    <div class="tab-pane fade overflow-auto" id="nav-editor-empty"></div>
                    <div class="tab-pane fade overflow-auto show active" id="nav-sqleditor">
                        <?php
                        /* 快捷查询 */
                        if ($curtable && !empty($config[FASTSQLS])) {
                            $fastsqls = SqlHelper::parseFastSqls($curtable, $config[FASTSQLS]);
                            if ($fastsqls) {
                                if ($action && isset($fastsqls[$action])) {
                                    $sqlstmt = $fastsqls[$action]['sql'];
                                    $sqlstmt = str_replace('{#object}', $curobject, $sqlstmt); // 替换{#object}参数
                                }
                                echo '<div class="btngroup" style="margin-bottom:2px">';
                                // 读取快捷方式的配置
                                if ($fastsqls) {
                                    echo '快捷查询：';
                                    $url = '?env=' . $curenv . '&db=' . $curdbname . '&table=' . $curtable . '&token='
                                        . $token . '&action=';
                                    foreach ($fastsqls as $name => $v) {
                                        echo '<a class="btn btn-sm btn-outline-info" style="padding:0 .5rem" href="';
                                        echo $url, $name, '" title="';
                                        echo (isset($v['remark']) ? $v['remark'] : ''), '">';
                                        echo (isset($v['text']) ? $v['text'] : $name), '</a>';
                                    }
                                }
                                echo '</div>';
                            }
                        }
                        /* 初始化界面 */
                        if (!$curobject && $action) {
                            if (0 === strpos($action, 'eg')) {
                                $sqlstmt = SqlHelper::getDefaultExampleSql($action);
                            } else {
                                /* 数据库操作 */
                                echo '<div class="location">当前数据库：<strong>', $curdbname, '</strong></div>';
                                $sqlstmt = SqlHelper::getDbExampleSql($action, $curdbname);
                            }
                        }
                        // sqlEditor表单
                        echo '<form id="sqleditorform" method="post" action="?env=', $curenv, '&db=', $curdbname,
                            '&token=',
                            $token,
                            '">';
                        echo '<textarea class="form-control form-control-sm code-editor" id="sqleditor" ',
                            'name="sqlstmt" rows="8" required="required" placeholder="';
                        echo HtmlHelper::getSqlEditorPlaceholder(), '" title="';
                        if ($curobject) {
                            echo '当前支持{#object}变量,可以自动替换为当前表名或程序名; 点击3次自动去除注释';
                        }
                        echo '">', $sqlstmt, '</textarea>';
                        ?>
                        <input type="hidden" name="env" value="<?php echo $curenv ?>" />
                        <input type="hidden" name="db" value="<?php echo $curdbname ?>" />
                        <input type="hidden" name="table" value="<?php echo $curtable ?>" />
                        <input type="hidden" name="token" value="<?php echo $token ?>" />
                        <button class="btn btn-primary btn-sm" type="submit">执行</button>
                        <button class="btn btn-secondary btn-sm" type="reset">重置</button>
                        <div class="btn-group btn-group-sm" role="group">
                            <a class="btn btn-light" onclick="return doEditorCmd('sqleditor','select');">全选</a>
                            <a class="btn btn-light" onclick="return doEditorCmd('sqleditor','copy');">复制</a>
                            <a class="btn btn-light" onclick="return doEditorCmd('sqleditor','paste');">粘贴</a>
                            <a class="btn btn-light" onclick="return doEditorCmd('sqleditor','clear');">清空</a>
                            <span class="btn text-danger" style="padding:.25rem 1rem;display:none;" id="exectimer">本次执行总耗时3秒</span>
                        </div>
                        <?php
                        if ($isprod) {
                            echo HtmlHelper::getCaptcha(); // 正式环境启用验证码防止删库跑路的悲剧
                        }
                        echo '</form>';
                        ?>
                    </div>
                    <div class="tab-pane fade overflow-auto" id="nav-mysaleditor">
                        <div class="row">
                            <div class="col-2">
                                <?php
                                // mysal目录的文件列表
                                echo '<div style="max-height:210px; overflow-y:auto" id="file-list">',
                                    '<ul class="list-unstyled">';
                                if (is_dir('./mysal')) {
                                    // 匿名方法四个参数：file:文件名,filepath:文件路径,level:目录层级,isfile:是否文件
                                    FileHelper::scandir('./mysal', true, '', '', function ($a, $b, $c, $d) use ($token) {
                                        $url = '###';
                                        if ($d) {
                                            $url = '?action=readfile&token=' . $token . '&path=' . $b;
                                        }
                                        echo '<li><a href="', $url, '" data-path="', $b, '">';
                                        echo ($c > 0 ? '|' : ''), str_repeat('--', $c), $a, '</a></li>';
                                    });
                                } else {
                                    echo '<li>路径[mysal]不存在!<br />请在命令行输入：<br />',
                                        '<code>php mysqladminlite.php --init</code><br />进行初始化后再刷新本页面</li>';
                                }
                                echo '</ul></div>';
                                ?>
                            </div>
                            <div class="col-10">
                                <form action="?action = savefile" id="file-form">
                                    <?php
                                    echo '<textarea class="form-control form-control-sm code-editor" id="file-editor" ',
                                        'name="file_content" rows="8" required="required"></textarea>';
                                    echo '<input type="hidden" name="token" value="', $token, '" />';
                                    ?>
                                    <input type="hidden" name="action" value="savefile" />
                                    <input type="hidden" name="path" id="file-path" value="" />
                                    <button class="btn btn-primary btn-sm" type="submit">保存</button>
                                    <button class="btn btn-secondary btn-sm" type="reset">重置</button>
                                    <div class="btn-group btn-group-sm" role="group">
                                        <a class="btn btn-light" onclick="return doEditorCmd('file-editor','select');">全选</a>
                                        <a class="btn btn-light" onclick="return doEditorCmd('file-editor','copy');">复制</a>
                                        <a class="btn btn-light" onclick="return doEditorCmd('file-editor','clear');">清空</a>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
                <!--/SQL编辑器-->
                <!--结果展示-->
                <?php
                // 显示命令执行结果(支持批量语句)
                $sqlstmts = [];
                if ($sqlstmt && strpos($sqlstmt, ' ') > 0) {
                    // 生成测试数据 2017-11-24
                    $sqlstmtcount = 0;
                    if (strpos($sqlstmt, 'testdata ') === 0) {
                        $sqlstmts = []; // 先清空,不然上面有残留数据
                        $sqlstmts[] = SqlHelper::parseSeedSqlStatement($sqlstmt);
                    } else {
                        // 普通命令解析 (textarea回车符号是\r,和OS平台无关)
                        $sqlstmts = SqlHelper::parseSqlStatement(explode("\r", $sqlstmt));
                    }
                }
                ?>
                <nav>
                    <div class="nav nav-tabs">
                        <a class="nav-item nav-link" id="nav-result-empty-tab" data-toggle="tab" href="#nav-result-empty" title="折叠此面板">&equiv;</a>
                        <a class="nav-item nav-link active" id="nav-result-tab" data-toggle="tab" href="#nav-result" title="(共<?php echo count($sqlstmts) ?>条命令)">结果展示</a>
                    </div>
                </nav>
                <div class="tab-content" id="nav-tabContent">
                    <div class="tab-pane fade overflow-auto" id="nav-result-empty"></div>
                    <div class="tab-pane fade overflow-auto show active" id="nav-result" style="max-height:500px;">
                        <?php
                        // 显示命令执行结果(支持批量语句)
                        if ($sqlstmts) {
                            $time_start = microtime(true); // 性能计时器
                            $pdoadapter->showQueryResultForWeb($sqlstmts, $curenv, $curdbname, $curtable, $token);
                            $time = microtime(true) - $time_start;
                            echo '<script>$("#exectimer").show().text("本次执行总耗时', round($time, 2),
                                '毫秒");</script>';
                        }
                        ?>
                    </div>
                </div>
                <!--/结果展示-->
                <script>
                    // 2017-10-28 服务器信息改为显隐控制，提高安全性，防止被窥探
                    $('#lnk-toggle-ipinfo').click(function() {
                        var $a = $(this);
                        if ($a.text() == '[...]') {
                            $a.text('[x]').prev().show();
                        } else {
                            $a.text('[...]').prev().hide();
                        }
                        return false;
                    })

                    // 展开或收起fieldset内容
                    $('.lnk-switch').click(function() {
                        var $a = $(this);
                        $a.parent().parent().find('.fieldset_content').toggle();
                    })
                    // 数据库切换, 服务器切换
                    $('#dbslt, #servslt').change(function() {
                        var val = $(this).val();
                        if (!confirm('您确定要跳转到 ' + val + ' 吗?')) {
                            return false;
                        }
                        if (val.length > 0) {
                            location.href = $(this).find('option[value="' + val + '"]').data('url');
                        }
                    })
                    // 侧边栏表格下拉框切换
                    $('#slt-table, #slt-view, #slt-procedure, #slt-function').change(function() {
                        var val = $(this).val();
                        if (val.length > 0) {
                            location.href = $(this).find('option[value="' + val + '"]').data('url');
                        }
                    })
                    // 选中表格行突出背景色
                    $('.table td').click(function() {
                        $(this).parent().toggleClass('bg-warning');
                    })
                    // 隐藏表格列
                    $('.table th a.lnk-hide').click(function() {
                        var $th = $(this).parent();
                        var idx = $th.index();
                        $th.hide().parent().parent().find('td:nth-child(' + (idx + 1) + ')').hide();
                    })
                    // 显示表格列
                    $('.table th a.lnk-show').click(function() {
                        var $th = $(this).parent();
                        $th.parent().parent().find('td, th').show();
                    })

                    // SQL编辑器验证码验证
                    $('#sqleditorform [type="submit"]').click(function() {
                        var sql = $.trim($('textarea[name="sqlstmt"]').val());
                        if (!sql) {
                            alert('未输入命令!')
                            return false;
                        }
                        // 2017-10-27 高危命令提醒
                        var firstword = sql.split(' ')[0].toLowerCase();
                        var isDanger = (firstword == 'drop' || firstword == 'truncate');
                        if ((firstword == 'delete' && sql.indexOf(' where ') < 0)) {
                            isDanger = true;
                        }
                        if (isDanger) {
                            var code = Math.ceil(Math.random() * 1000);
                            var msg = '您将执行的是高危命令！<br />为避免类似删库跑路的惨剧发生，' +
                                '请输入安全码确认操作！<br />安全码是：' + code;
                            var inputCode = prompt(msg.replace(/<br \/>/g, "\n"));
                            if (!inputCode) {
                                return false;
                            }
                            if (inputCode != code) {
                                alert('安全码输入错误!');
                                return false;
                            }
                        }
                        // 正式环境验证码
                        var $captcha = $('input[name="captcha"]');
                        if ($captcha[0]) {
                            if ($captcha.val() != $captcha.prop('placeholder')) {
                                $('#error').text('验证码错误');
                                $captcha.val('');
                                return false;
                            }
                            if (!confirm('当前为生产环境，您确认执行此操作吗？')) {
                                return false;
                            }
                        }
                        return true;
                    })
                    // 2017-11-3 验证码点击3次自动填表
                    $('input[name="captcha"]').click(function() {
                        var $input = $(this);
                        var n = $input.data('clickCount');
                        if (!n) {
                            n = 0;
                        }
                        n++;
                        if (n % 3 == 0) {
                            n = 0;
                            $input.val($input.prop('placeholder'));
                        }
                        $input.data('clickCount', n);
                    })
                    // sql编辑器点击3次自动去除注释符号(--或eg.)
                    $('textarea[name="sqlstmt"]').click(function() {
                        var $ta = $(this);
                        var txt = $ta.val();
                        if (!txt) {
                            return;
                        }
                        var n = $ta.data('clickcount');
                        if (!n) {
                            n = 0;
                        }
                        n++;
                        $ta.data('clickcount', n);
                        if (n % 3 == 0) {
                            var has_run = false;
                            var atxt = txt.split("\n");
                            var stxt = '';
                            var sline = '';
                            var len = 0;
                            var strlen3 = '';
                            for (var i in atxt) {
                                sline = $.trim(atxt[i]);
                                len = sline.length;
                                strlen3 = sline.substring(0, 3);
                                if (len > 4 && (sline.substring(0, 4) == 'eg. ')) {
                                    stxt += sline.substring(4) + "\n";
                                    has_run = true;
                                } else if (len > 3 && (strlen3 == '-- ') || (strlen3 == 'eg.')) {
                                    stxt += sline.substring(3) + "\n";
                                    has_run = true;
                                } else if (len > 2 && sline.substring(0, 2) == '--') {
                                    stxt += sline.substring(2) + "\n";
                                    has_run = true;
                                } else {
                                    stxt += sline + "\n";
                                }
                            }
                            if (has_run) { // 有替换操作才重新复制,解决无需替换的时候,鼠标点击三次光标会自动移动到最后的问题
                                $ta.val(stxt);
                            }
                        }
                    })
                    // 在当前SQL编辑窗口显示示例语句 2017-11-23
                    $('.btn_example_sql').click(function() {
                        var sql = $(this).data('sql');
                        if (!sql) {
                            return false;
                        }
                        var $textarea = $('textarea[name="sqlstmt"]');
                        if (!$textarea.val()) {
                            $textarea.val(sql);
                        } else {
                            $textarea.val($textarea.val() + "\n\n" + sql);
                        }
                        return false;
                    })
                    // SQL编辑器的复制、粘帖等功能
                    function doEditorCmd(elid, cmd) {
                        var el = document.getElementById(elid);
                        switch (cmd) {
                            case 'copy':
                                el.select();
                                document.execCommand("Copy");
                                alert("内容复制成功！");
                                break;
                            case 'paste':
                                alert('该功能尚未支持! 请使用快捷键 [ctrl+v]');
                                el.focus();
                                break;
                            case 'select':
                                el.select();
                                break;
                            case 'clear':
                                el.value = '';
                                break;
                        }
                        return false;
                    }
                    // 加载mysal目录的文件
                    $('#file-list a').click(function(event) {
                        var url = $(this).attr('href');
                        if (url.indexOf('###') > -1) {
                            return false;
                        }
                        $('#file-path').val($(this).data('path'));
                        $('#file-editor').load(url, function(data) {
                            // jquery load 陷阱 www.cnblogs.com/whatlonelytear/p/7887059.html
                            $('#file-editor').val('');
                            $('#file-editor').val(data);
                        });
                        return false;
                    });
                    // 修改并保存mysal目录的文件
                    $('#file-form .btn-primary').click(function(event) {
                        //event.preventDefault();
                        var $form = $('#file-form');
                        $.post($form.attr('action'), $form.serialize(), function(data) {
                            // console.log(data);
                            alert('保存成功！');
                        })
                        return false;
                    })
                </script>
</body>

</html>
<?php
/**
 * 数据库通用类
 * @version 2017-8-23 Added.
 * @version 2017-12-15 增加导出方法和解析服务器信息的方法
 */
class PdoConnector
{

    /**
     * 数据库连接DSN
     * @var type
     */
    protected $dsn = '';

    /**
     * 数据库连接对象
     * @var type
     */
    protected $link = null;
    /**
     * 数据库环境信息, 数据库名称，数据库帐号等
     * host,database,user,password,port,charset
     * @var array
     */
    protected $envInfo = [];

    /**
     * 构造方法
     * @param array $dbcfg
     * @throws Exception
     */
    public function __construct(array $dbcfg)
    {
        $this->envInfo = $dbcfg;
        $this->dsn = $this->getDSN($dbcfg);
        if (empty($this->dsn)) {
            throw new \Exception('数据库配置文件无效!');
        }
        try {
            $this->link = new \PDO($this->dsn, $dbcfg['user'], $dbcfg['password']);
            $this->link->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); // 提高错误级别
        } catch (\PDOException $ex) {
            // 命令行环境下错误消息显示不出来的解决方法：
            // 设置CMD环境编码为GBK:936,utf-8:65001，否则PDO实例化异常消息会乱码
            // exec("CHCP 936");
            // 浏览器环境下错误消息显示不出来的解决方法：
            // header("Content-Type:text/html;charset=utf-8");
            // 通用解决方法(直接转码)：
            // echo iconv('gbk', 'utf-8', $ex->getMessage());
            throw new \Exception('数据库连接失败：请检查数据库是否可用或数据库连接信息是否正确 [' . $this->dsn . ']');
        }
    }

    public function getDSN(array $server)
    {
        // 解析host和port是否有端口转发
        $host = $server['host'];
        $port = $server['port'];
        // 数据库类型
        $adapter = isset($server['adapter']) ? $server['adapter'] : 'mysql';
        switch ($adapter) {
            case 'mysql':
                $dsnarr = ['host' => $host, 'port' => $port];
                if (isset($server['charset'])) {
                    $dsnarr['charset'] = $server['charset'];
                }
                if (!empty($server['database'])) {
                    $dsnarr['dbname'] = $server['database'];
                }
                $dsn = '';
                foreach ($dsnarr as $name => $value) {
                    $dsn .= $name . '=' . $value . ';';
                }
                $dsn = 'mysql:' . $dsn;
                break;
            default:
                $dsn = '';
                break;
        }
        return $dsn;
    }
    /**
     * 返回数据库服务器环境变量的值
     * host,database,user,password,port,charset
     * @param string $name 名称
     * @param mixed $defv 默认值
     */
    public function getEnvInfo($name, $defv = '')
    {
        if (!$name) {
            return $this->envInfo;
        }
        return isset($this->envInfo[$name]) ? $this->envInfo[$name] : $defv;
    }

    /**
     * 执行一条预处理语句
     * 这里不能用foreach，参考bindParam陷阱(值参数是引用型变量)：http://www.laruence.com/2012/10/16/2831.html
     * @param string $sqlstmt SQL 命令
     * @param array $params 参数值
     */
    public function execute($sqlstmt, $params = [])
    {
        try {
            $stmt = $this->link->prepare($sqlstmt);
            if ($params) {
                $stmt->execute($params);
            } else {
                $stmt->execute();
            }
            return $stmt->rowCount();
        } catch (\PDOException $ex) {
            throw $ex;
        }
    }
    /**
     * 执行一条SQL语句，并返回受影响的行数
     * @param string $sqlstmt
     */
    public function exec($sqlstmt)
    {
        try {
            return $this->link->exec($sqlstmt);
        } catch (\PDOException $ex) {
            // write log
            throw $ex;
        }
    }

    /**
     * 执行SQL 命令并返回一行
     * 这里不能用foreach，参考bindParam陷阱(值参数是引用型变量)：http://www.laruence.com/2012/10/16/2831.html
     * @param string $sqlstmt SQL 命令
     * @param array $params 参数值
     */
    public function find($sqlstmt, $params = [])
    {
        $stmt = $this->link->prepare($sqlstmt);
        try {
            if ($params) {
                $stmt->execute($params);
            } else {
                $stmt->execute();
            }
            return $stmt->fetch(\PDO::FETCH_ASSOC);
        } catch (\PDOException $ex) {
            // write log
            throw $ex;
        }
    }

    /**
     * 执行SQL 命令并返回结果集
     * 这里不能用foreach，参考bindParam陷阱(值参数是引用型变量)：http://www.laruence.com/2012/10/16/2831.html
     * @param string $sqlstmt SQL 命令
     * @param array $params 参数值
     */
    public function query($sqlstmt, $params = [], $fetch_style = \PDO::FETCH_ASSOC)
    {
        $stmt = $this->link->prepare($sqlstmt);
        try {
            if ($params) {
                $stmt->execute($params);
            } else {
                $stmt->execute();
            }
            $list = $stmt->fetchAll($fetch_style);
            return $list;
        } catch (\PDOException $ex) {
            throw $ex;
        }
    }

    /**
     * 批处理执行SQL语句
     * 批处理的指令都认为是execute操作
     * @param array $asqls SQL批处理指令
     * @param boolean $isexecute 指示方法是执行或查询
     * @param boolean $isrollback 遇到错误是否回滚
     * @return array 错误消息数组
     * @version 1.0 2016-6-15 xcaller Added.
     * @example batchQuery($asql);
     */
    public function batchQuery($asqls = [], $isexecute = false, $isrollback = false)
    {
        // 错误消息数组
        $outdata = ['status' => 0, 'affected_rows' => 0, 'data' => ['没有任何命令']];
        if (!$asqls) {
            return $outdata;
        }
        // 如果参数是单条SQL 命令，则自动封装为数组
        if (!is_array($asqls)) {
            $asqls = array($asqls);
        }
        $outdata['data'] = []; // 清空错误消息数组
        $effectrows = 0; // 受影响的行数
        // 自动启动事务支持，提高执行性能        
        $this->link->beginTransaction(); // mysql_query("BEGIN");
        try {
            foreach ($asqls as $sql) {
                if (!trim($sql)) {
                    continue;
                }
                if ($isexecute) {
                    $result = $this->link->exec($sql);
                    $effectrows += $result;
                } else {
                    $result = $this->link->query($sql);
                }
                // 错误处理
                if (false === $result) {
                    // [SQLSTATE error code, Driver-specific error code, Driver-specific error message] | mysql_error();
                    $errorInfo = $this->link->errorInfo();
                    $result['data'][] = $errorInfo[2];
                    // 发生错误自动回滚事务
                    if ($isrollback) {
                        $this->link->rollBack(); // mysql_query("ROLLBACK");
                    }
                }
            }
            // 提交事务
            $this->link->commit(); // mysql_query("COMMIT"); mysql_query("END");
            $outdata['status'] = 1;
            $outdata['affected_rows'] = $effectrows;
            return $outdata;
        } catch (\PDOException $ex) {
            // 回滚事务
            $this->link->rollBack(); // mysql_query("ROLLBACK"); mysql_query("END");
            throw $ex;
        }
    }
    /**
     * 返回数据库版本号
     * @return string 数据库版本号
     */
    public function getDbVersion()
    {
        $versioninfo = $this->find('select version() as `version`;');
        return $versioninfo['version'];
    }
    /**
     * 返回数据库配置变量列表
     * @return array 配置变量列表
     */
    public function getDbVariables()
    {
        $sqlstmt = 'show global variables';
        try {
            $list = $this->query($sqlstmt);
            return $list;
        } catch (\PDOException $ex) {
            return [['Variable_name' => '查询数据库信息出错：', 'Value' => $ex->getMessage()]];
        }
    }

    /**
     * 返回数据库信息
     * @param string $dbname
     * @return array
     */
    public function getDbInfo($dbname)
    {
        $list = $this->getDatabases($dbname);
        return is_array($list) ? current($list) : [];
    }

    /**
     * 返回所有数据库
     * @param string $dbname 数据库名称
     * @return array SCHEMA_NAME,DEFAULT_CHARACTER_SET_NAME,DEFAULT_COLLATION_NAME
     */
    public function getDatabases($dbname = '')
    {
        $sqlstmt = 'select SCHEMA_NAME,DEFAULT_CHARACTER_SET_NAME,DEFAULT_COLLATION_NAME from '
            . 'information_schema.schemata where SCHEMA_NAME';
        if ($dbname) {
            $sqlstmt .= ' = ?';
            $params = [$dbname];
        } else {
            $sqlstmt .= ' not in (\'mysql\',\'information_schema\',\'performance_schema\');';
            $params = [];
        }
        return $this->query($sqlstmt, $params);
    }
    /**
     * 选择数据库
     */
    public function selectDb($dbname)
    {
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        }
        $this->execute('use ' . $dbname);
    }

    /**
     * 返回所有数据表
     * @param string $dbname
     * @param string $tablename
     * @return array TABLE_NAME, TABLE_TYPE, ENGINE, DATA_LENGTH, CREATE_TIME, TABLE_COLLATION, TABLE_COMMENT
     */
    public function getTables($dbname, $tablename = '')
    {
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        }

        $sqlstmt = 'select TABLE_NAME, TABLE_TYPE, ENGINE, DATA_LENGTH, CREATE_TIME, TABLE_COLLATION, TABLE_COMMENT '
            . 'from information_schema.tables where TABLE_SCHEMA=:database AND TABLE_TYPE=\'BASE TABLE\'';
        $params = ['database' => $dbname];
        if ($tablename) {
            $sqlstmt .= ' AND TABLE_NAME = :table';
            $params['table'] = $tablename;
        }
        return $this->query($sqlstmt, $params);
    }

    /**
     * 返回数据表元数据
     * @param string $dbname
     * @param string $tablename
     * @param string $field
     * @return array TABLE_NAME, TABLE_TYPE, ENGINE, DATA_LENGTH, CREATE_TIME, TABLE_COLLATION, TABLE_COMMENT
     */
    public function getTableInfo($dbname, $tablename, $field = '')
    {
        $list = $this->getTables($dbname, $tablename);
        if (is_array($list)) {
            $data = current($list);
            if ($field && isset($data[$field])) {
                return $data[$field];
            }
            return $data;
        }
        return null;
    }
    /**
     * 返回数据表记录数量少于指定数量的表集合
     * @param string $dbname
     * @param int $eltcount
     */
    public function getEmptyTables($dbname, $eltcount = 0, $prefix = '')
    {
        $this->checkDbName($dbname);
        $list = $this->query('show tables;');
        $tables = [];
        $table = '';
        foreach ($list as $row) {
            $table = current($row);
            if ($prefix && false === strpos($table, $prefix)) {
                continue;
            }
            $count = $this->count($table);
            if ($count > $eltcount) {
                continue;
            }
            $tables[$table] = $row;
        }
        return $tables;
    }
    /**
     * 判断数据表是否存在
     */
    public function hasTable($tablename, $dbname = '')
    {
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        }
        $sqlstmt = 'select count(*) from information_schema.tables where TABLE_SCHEMA=:database AND TABLE_NAME=:table;';
        $stmt = $this->link->prepare($sqlstmt);
        $stmt->execute(['database' => $dbname, 'table' => $tablename]);
        return current($stmt->fetch(\PDO::FETCH_ASSOC)) > 0;
    }
    /**
     * 查询数据表记录数量
     * @param string $table
     */
    public function count($table)
    {
        $sqlstmt = "select count(*) as n from $table;";
        $stmt = $this->link->prepare($sqlstmt);
        try {
            $stmt->execute();
            return current($stmt->fetch(\PDO::FETCH_ASSOC));
        } catch (\PDOException $ex) {
            throw $ex;
        }
    }

    /**
     * 返回所有索引
     * @param string $dbname
     * @param string $tablename
     * @return array Table, Non_unique, Key_name, Seq_in_index, Column_name, Collation, Cardinality, Sub_part, Packed,
     *          Null, Index_type
     */
    public function getIndexes($dbname, $tablename)
    {
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        }
        $list = $this->query('show index from ' . filter_var($dbname) . '.' . filter_var($tablename));
        return $list;
    }

    /**
     * 返回所有列
     * @param string $dbname
     * @param string $tablename
     * @return array COLUMN_NAME, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE,
     *      EXTRA, COLUMN_DEFAULT, COLUMN_TYPE, COLUMN_KEY, COLUMN_COMMENT
     */
    public function getColumns($dbname, $tablename)
    {
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        }
        $sqlstmt = 'select COLUMN_NAME, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, '
            . 'NUMERIC_SCALE, EXTRA, COLUMN_DEFAULT, COLUMN_TYPE, COLUMN_KEY, COLUMN_COMMENT from information_schema.'
            . 'columns WHERE TABLE_SCHEMA=:database AND TABLE_NAME=:table order by ORDINAL_POSITION';
        $list = $this->query($sqlstmt, ['database' => $dbname, 'table' => $tablename]);
        return $list;
    }

    /**
     * 返回所有视图
     * @param string $dbname
     * @return array TABLE_NAME
     */
    public function getViews($dbname)
    {
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        }
        $sqlstmt = 'select TABLE_NAME from information_schema.views where TABLE_SCHEMA=:database;';
        $list = $this->query($sqlstmt, ['database' => $dbname]);
        return $list;
    }

    /**
     * 返回所有程序
     * @param string $dbname
     * @param string $type
     * @return array ROUTINE_NAME, ROUTINE_TYPE
     */
    public function getRoutines($dbname, $type = '')
    {
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        }
        if ($type) {
            $type = strtoupper($type);
        }
        $sqlstmt = 'select ROUTINE_NAME, ROUTINE_TYPE from information_schema.routines WHERE ROUTINE_SCHEMA=:database'
            . ($type ? " and ROUTINE_TYPE='$type'" : '') . ' order by ROUTINE_TYPE;';
        $list = $this->query($sqlstmt, ['database' => $dbname]);
        return $list;
    }

    /**
     * 返回程序的所有参数
     * <br />ORDINAL_POSITION=0为返回参数
     * @param string $dbname
     * @param string $routine
     * @return array ORDINAL_POSITION, PARAMETER_MODE, PARAMETER_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH,
     *          DTD_IDENTIFIER
     */
    public function getRoutineParams($dbname, $routine)
    {
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        }
        $sqlstmt = 'select ORDINAL_POSITION, PARAMETER_MODE, PARAMETER_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, '
            . 'DTD_IDENTIFIER, ROUTINE_TYPE from information_schema.PARAMETERS where SPECIFIC_SCHEMA=:database and '
            . 'SPECIFIC_NAME=:routine';
        $list = $this->query($sqlstmt, ['database' => $dbname, 'routine' => $routine]);
        return $list;
    }
    /**
     * 返回程序的所有参数定义
     * @param string $dbname
     * @param string $routine
     * @return array
     */
    public function getRoutineParamsSimple($dbname, $routine)
    {
        $list = $this->getRoutineParams($dbname, $routine);
        $outlist = [];
        foreach ($list as $row) {
            $objname = $row['PARAMETER_NAME'];
            $datatype = $row['DATA_TYPE'];

            if ($objname) {
                $outlist[] = $row['PARAMETER_MODE'] . ' ' . $objname . ' '
                    . ($datatype == 'varchar' ? $row['DTD_IDENTIFIER'] : $datatype);
            } else {
                $outlist[] = 'returns: ' . ($datatype == 'varchar' ? $row['DTD_IDENTIFIER'] : $datatype);
            }
        }
        return $outlist;
    }

    /**
     * 格式化sql脚本
     * <br />去除表注释、字段注释、多余空格、drop命令
     * @param string $str
     * @param string $break_line
     * @return string
     */
    public function formatSqlscript($str, $break_line = '\r')
    {
        $str = preg_replace("/ comment '[^']*'/", '', $str); // comment '...'
        $str = preg_replace("/ comment='[^']*'/", '', $str); // comment='...'
        $str = preg_replace("/--[^$break_line]+/", '', $str); // -- ...
        $str = preg_replace("/\/\*[^\/]+\//", '', $str); // /*...*/
        $str = preg_replace("/drop[^;]+;/", '', $str); // drop table ...;
        $str = preg_replace('/[\s]+/', ' ', trim($str)); // 多个空格合并成一个
        return $str;
    }

    /**
     * 导出数据库表结构到SQL文件
     * @param string $dbname
     * @param string $tablename
     * @param boolean $isEachFile
     * @param string $filename
     * @param string $tablePrefix
     * @version 2020-2-6
     */
    public function exportDbSchemaAsSqlForCli($dbname, $tablename, $isEachFile, $filename = '', $tablePrefix = '')
    {
        $option = ['isDownload' => false, 'isEachFile' => $isEachFile];
        $this->exportDbSchemaAsSql($dbname, $tablename, $option, $filename, $tablePrefix);
    }
    /**
     * 导出数据库表结构到SQL文件
     * @param string $dbname
     * @param string $tablename
     */
    public function exportDbSchemaAsSqlForDownload($dbname, $tablename = '')
    {
        $option = ['isDownload' => true, 'isEachFile' => false];
        $this->exportDbSchemaAsSql($dbname, $tablename, $option, '', '');
    }
    /**
     * 导出数据库表结构到SQL文件
     * @param string $dbname
     * @param string $tablename
     * @param array $option
     * @param string $filename
     * @param string $tablePrefix
     * @version 2020-2-6
     */
    private function exportDbSchemaAsSql($dbname, $tablename, $option, $filename = '', $tablePrefix = '')
    {
        $defaultoption = ['isDownload' => true, 'isEachFile' => true];
        extract(array_merge($defaultoption, $option));
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        }
        if (!$filename) {
            $filename = $tablename ? $tablename : $dbname;
        }
        if ($isDownload) {
            header("Content-type:application/octet-stream");
            header('Content-Disposition:attachment; filename=mysal-' . $filename . '.sql');
        }
        $list = [];
        if ($tablename) {
            $list[] = ['TABLE_NAME' => $tablename];
        } else {
            $list = $this->getTables($dbname);
        }
        // 动态切换数据库
        if ($dbname != $this->envInfo['database']) {
            $this->selectDb($dbname);
        }
        $content = '';
        $objname = '';
        foreach ($list as $row) {
            $objname = $row['TABLE_NAME'];
            if ($tablePrefix && false === strpos($objname, $tablePrefix)) {
                continue;
            }
            $createsql = $this->find('show create table ' . $objname . ';');
            $createsql = trim($createsql['Create Table']);
            if (substr($createsql, -1) != ';') {
                $createsql .= ';';
            }
            $content .= 'DROP TABLE IF EXISTS `' . $objname . '`;' . PHP_EOL;
            $content .= $createsql . PHP_EOL . PHP_EOL;
            if ($isEachFile && !$isDownload) {
                file_put_contents($filename . $objname . '.sql', $content);
                $content = '';
            }
        }
        unset($list);
        if ($isDownload) {
            echo $content;
            exit();
        } else {
            if (!$isEachFile) {
                file_put_contents($filename, $content);
            }
        }
    }

    /**
     * 导出数据库表结构到 Markdown 文件
     * @param string $dbname
     * @version 2017-12-15
     */
    public function exportDbSchemaAsMarkdown($dbname, $prefix = '')
    {
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        }
        header("Content-type:application/octet-stream");
        header("Content-Disposition:attachment; filename=mysal-$dbname.md");
        // 动态切换数据库
        if ($dbname != $this->envInfo['database']) {
            $this->selectDb($dbname);
        }
        $list = $this->getTables($dbname);
        foreach ($list as $row) {
            $objname = $row['TABLE_NAME'];
            if ($prefix && false === strpos($objname, $prefix)) {
                continue;
            }
            $columns = $this->getColumns($dbname, $objname);
            if ($columns) {
                echo '### ', $objname, PHP_EOL, PHP_EOL;
                echo '| 字段 | 数据类型 | 可空 | 默认值 | 索引键 | 备注 |', PHP_EOL;
                echo '| --- | --- | --- | --- | --- |', PHP_EOL;
                foreach ($columns as $col) {
                    echo '| ', $col['COLUMN_NAME'], ' | ', $col['COLUMN_TYPE'], ' | ', $col['IS_NULLABLE'], ' | ';
                    echo $col['COLUMN_DEFAULT'], ' | ', $col['COLUMN_KEY'], ' | ';
                    echo SqlHelper::parseDbComment($col['COLUMN_COMMENT']), ' |', PHP_EOL;
                }
                echo PHP_EOL, PHP_EOL;
            }
        }
        unset($list);
        exit();
    }

    /**
     * 导出数据库表结构到 Excel 文件
     * @param string $dbname 数据库名称
     * @param string $filename 文件名
     * @param string $tablePrefix 数据表前缀
     */
    public function exportDbSchemaAsExcelForCli($dbname, $filename = '', $tablePrefix = '')
    {
        $option = ['isExcel' => true, 'isDownload' => false, 'isLocalFile' => true];
        $this->exportDbSchemaAsExcel($dbname, $option, $filename, $tablePrefix);
    }

    /**
     * 导出数据库表结构到 Excel 文件
     * @param string $dbname 数据库名称
     * @param string $filename 文件名
     * @param string $tablePrefix 数据表前缀
     */
    public function exportDbSchemaAsHtmlForCli($dbname, $filename = '', $tablePrefix = '')
    {
        $option = ['isExcel' => false, 'isDownload' => false, 'isLocalFile' => true];
        $this->exportDbSchemaAsExcel($dbname, $option, $filename, $tablePrefix);
    }
    /**
     * 导出数据库表结构到 Excel 文件
     * @param string $dbname 数据库名称
     */
    public function exportDbSchemaAsHtmlOnline($dbname)
    {
        $option = ['isExcel' => false, 'isDownload' => false, 'isLocalFile' => false];
        $this->exportDbSchemaAsExcel($dbname, $option, '', '');
    }
    /**
     * 导出数据库表结构到 Excel 文件
     * @param string $dbname 数据库名称
     */
    public function exportDbSchemaAsHtmlForDownload($dbname)
    {
        $option = ['isExcel' => false, 'isDownload' => true, 'isLocalFile' => false];
        $this->exportDbSchemaAsExcel($dbname, $option, '', '');
    }
    /**
     * 导出数据库表结构到 Excel 文件
     * @param string $dbname 数据库名称
     */
    public function exportDbSchemaAsExcelForDownload($dbname)
    {
        $option = ['isExcel' => true, 'isDownload' => true, 'isLocalFile' => false];
        $this->exportDbSchemaAsExcel($dbname, $option, '', '');
    }
    /**
     * 导出数据库表结构到 Excel 文件
     * @param string $dbname 数据库名称
     * @param array $option 选项参数
     * @param string $filename 文件名
     * @param string $tablePrefix 数据表前缀
     * @version 2020-1-13
     */
    private function exportDbSchemaAsExcel($dbname, $option, $filename = '', $tablePrefix = '')
    {
        $defaultoption = ['isExcel' => true, 'isDownload' => true, 'isLocalFile' => false];
        extract(array_merge($defaultoption, $option));
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        }
        if (!$filename) {
            $filename = 'mysal-' . $dbname . ($isExcel ? '.xls' : '.html');
        }
        if ($isDownload) {
            HtmlHelper::setExportHeader($isExcel, $filename);
        }
        $content = '';
        if ($isExcel) {
            $content .= '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:'
                . 'office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta http-equiv=Content-Type content='
                . '"text/html; charset=utf-8"></head><body>';
        } else {
            $content .= '<html><head><title>数据库 ' . $dbname . ' 表结构</title><style type="text/css">body{color:#333;'
                . 'font-size:12px;}h3{margin:5px 0;}h3 a{color:#333;}h3 small{font-size:12px;color:#aaa;margin-left:'
                . '10px;}section{width:400px;height:250px;margin:0 10px 10px 0;float:left;}.content{height:210px;'
                . 'overflow:auto;}table{width:100%; border:solid 1px #CCC;border-top:none;border-left:none;}th'
                . '{white-space:nowrap;}th,td{padding:3px;font-size:12px;border:solid 1px #CCC;border-right:none;'
                . 'border-bottom:none;}</style></head></body>';
        }
        // 动态切换数据库
        if ($dbname != $this->envInfo['database']) {
            $this->selectDb($dbname);
        }
        $list = $this->getTables($dbname);
        $content .= '<h1>数据库 ' . $dbname . ' 表结构</h1><div class="summary">日期：' . date('Y-m-d H:i:s')
            . '<br />数据表：' . count($list) . '个</div>';
        foreach ($list as $row) {
            $objname = $row['TABLE_NAME'];
            if ($tablePrefix && false === strpos($objname, $tablePrefix)) {
                continue;
            }
            $columns = $this->getColumns($dbname, $objname);
            $indexes = $this->getIndexes($dbname, $objname);
            $content .= '<section><h3>';
            if (!$isExcel) {
                $content .= '<a href="#" onclick="this.parentNode.parentNode.style.display=\'none\';return false;" '
                    . 'title="隐藏此模型">[x]</a> ';
            }
            $content .= $objname . ' <small>';
            $content .= ($row['TABLE_COMMENT'] ? ('(' . $row['TABLE_COMMENT'] . ') ') : '');
            $content .= count($columns) . ' 个字段</small></h3>';
            if ($columns) {
                if ($isExcel) {
                    $content .= '<table border="1">';
                } else {
                    $content .= '<div class="content"><table cellspacing="0" cellpadding="0">';
                }
                $content .= '<tr><th>--</th><th>字段</th><th>数据类型</th><th>可空</th><th>默认值</th><th>索引键</th>'
                    . '<th>备注</th></tr>';
                $i = 0;
                foreach ($columns as $col) {
                    $i++;
                    $content .= '<tr><td>' . $i . '</td><td>' . $col['COLUMN_NAME'] . '</td><td>' . $col['COLUMN_TYPE']
                        . '</td><td>' . $col['IS_NULLABLE'] . '</td><td>' . $col['COLUMN_DEFAULT'] . '</td><td>'
                        . $col['COLUMN_KEY'] . '</td><td>' . SqlHelper::parseDbComment($col['COLUMN_COMMENT'])
                        . '</td></tr>';
                }
                $content .= '</table>';
            }
            if ($indexes) {
                $content .= $isExcel ? '<table border="1">' : '<table cellspacing="0" cellpadding="0" '
                    . 'style="margin-top:1px"><tr><th>索引名称</th><th>列名称</th></tr>';
                foreach ($indexes as $idx) {
                    $content .= '<tr><td>' . $idx['Key_name'] . '</td><td>' . $idx['Column_name'] . '</td></tr>';
                }
                $content .= '</table>';
            }
            $content .= '</div></section>';
        }
        $content .= '</body></html>';
        if ($isLocalFile) {
            file_put_contents($filename, $content);
        } else {
            echo $content;
            exit();
        }
    }

    /**
     * 导出数据库所有表结构到数据种子文件
     * @param string $dbname 数据库名称
     * @param string $tablename 数据表名称
     * @param string $framework 数据种子框架
     * @param string $dir 种子文件目录路径
     * @param boolean $overwrite 是否覆盖
     * @version 2020-2-16
     */
    public function exportDbSchemaAsSeed(
        $dbname,
        $framework = 'phinx',
        $dir = '',
        $overwrite = false,
        $eltcount = 0,
        $prefix = ''
    ) {
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        }
        // 动态切换数据库
        if ($dbname != $this->envInfo['database']) {
            $this->selectDb($dbname);
        }
        if ($eltcount > 0) {
            $list = $this->getEmptyTables($dbname, $eltcount);
        } else {
            $list = $this->getTables($dbname);
        }
        if (!$list) {
            return [0, 0, 0];
        }
        $ido = 0;
        $i = 0;
        $objname = '';
        foreach ($list as $row) {
            $objname = $row['TABLE_NAME'];
            if ($prefix && false === strpos($objname, $prefix)) {
                continue;
            }
            $result = $this->exportTableSchemaAsSeedForCli(
                $dbname,
                $objname,
                $framework,
                ($dir . mysal_table2classname($objname) . 'Seeder.php'),
                $overwrite
            );
            if ($result) {
                $ido++;
            }
            $i++;
        }
        return [$i, $ido, $i - $ido];
    }


    /**
     * 导出表结构到 Excel 文件
     * @param string $dbname 数据库名称
     * @param string $tablename 数据表名称
     * @param string $filename 文件名
     */
    public function exportTableSchemaAsExcelForCli($dbname, $tablename, $filename = '')
    {
        $option = ['isExcel' => true, 'isDownload' => false, 'isLocalFile' => true];
        $this->exportTableSchemaAsExcel($dbname, $tablename, $option, $filename);
    }

    /**
     * 导出表结构到 Excel 文件
     * @param string $dbname 数据库名称
     * @param string $tablename 数据表名称
     * @param string $filename 文件名
     */
    public function exportTableSchemaAsHtmlForCli($dbname, $tablename, $filename = '')
    {
        $option = ['isExcel' => false, 'isDownload' => false, 'isLocalFile' => true];
        $this->exportTableSchemaAsExcel($dbname, $tablename, $option, $filename);
    }

    /**
     * 导出表结构到 Excel 文件
     * @param string $dbname 数据库名称
     * @param string $tablename 数据表名称
     */
    public function exportTableSchemaAsHtmlOnline($dbname, $tablename)
    {
        $option = ['isExcel' => false, 'isDownload' => false, 'isLocalFile' => false];
        $this->exportTableSchemaAsExcel($dbname, $tablename, $option, '');
    }

    /**
     * 导出表结构到 Excel 文件
     * @param string $dbname 数据库名称
     * @param string $tablename 数据表名称
     */
    public function exportTableSchemaAsHtmlForDownload($dbname, $tablename)
    {
        $option = ['isExcel' => false, 'isDownload' => true, 'isLocalFile' => false];
        $this->exportTableSchemaAsExcel($dbname, $tablename, $option, '');
    }
    /**
     * 导出表结构到 Excel 文件
     * @param string $dbname 数据库名称
     * @param string $tablename 数据表名称
     */
    public function exportTableSchemaAsExcelForDownload($dbname, $tablename)
    {
        $option = ['isExcel' => true, 'isDownload' => true, 'isLocalFile' => false];
        $this->exportTableSchemaAsExcel($dbname, $tablename, $option, '');
    }
    /**
     * 判断数据库名称是否是当前数据库,不一致则切换到该数据库
     * @param string $dbname 参数指定的数据库名称
     * @return string 数据库名称
     */
    private function checkDbName($dbname)
    {
        if (!$dbname) {
            $dbname = $this->envInfo['database'];
        } else {
            // 动态切换数据库
            if ($dbname != $this->envInfo['database']) {
                try {
                    $this->selectDb($dbname);
                } catch (\PDOException $ex) {
                    exit($ex->getMessage());
                }
            }
        }
        return $dbname;
    }
    /**
     * 导出表结构到 Excel 文件
     * @param string $dbname 数据库名称
     * @param string $tablename 数据表名称
     * @param array $option 选项参数
     * @param string $filename 文件名
     * @version 2020-2-7
     */
    private function exportTableSchemaAsExcel($dbname, $tablename, $option, $filename = '')
    {
        $defaultoption = ['isExcel' => true, 'isDownload' => true, 'isLocalFile' => false];
        extract(array_merge($defaultoption, $option));
        $dbname = $this->checkDbName($dbname);
        if ($isExcel) {
            $fileExt = '.xls';
            $content = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:'
                . 'office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta http-equiv=Content-Type content="'
                . 'text/html; charset=utf-8"></head><body>';
            $tableBegin = '<div><table border="1">';
            $tableBeginIndex = '<table border="1">';
        } else {
            $fileExt = '.html';
            $content = '<html><head><title>数据表 ' . $tablename . ' 模型信息</title><style type="text/css">body'
                . '{color:#333;font-size:12px;}h1 div{font-size:13px;}h3{margin-bottom:5px;}h3 a{color:#333;}'
                . 'h3 small{font-size:12px;color:#aaa;margin-left:10px;}section{max-width:900px;}'
                . 'th{white-space:nowrap;}td,th{font-size:13px;}.grid{border:solid 1px #CCC;border-bottom:none;'
                . 'border-right:none;}.grid tr:hover{background-color:#efefef;}.grid tr.active{background-color:#fc0;}'
                . '.grid th,.grid td{border:solid 1px #CCC;border-left:none;border-top:none;padding:5px;}.grid th'
                . '{word-break:keep-all;}.grid th a{font-weight:normal;margin-left:3px;}.bluegrid tr:nth-child(even)'
                . '{background:#e6e6e6;}.bluegrid th{background:rgb(81,130,187);color:#FFF;}.bluegrid th a{color:#FFF;}'
                . '</style></head></body>';
            $tableBegin = '<div class="content"><table class="grid bluegrid" cellspacing="0" cellpadding="0">';
            $tableBeginIndex = '<table class="grid bluegrid" cellspacing="0" cellpadding="0">';
        }
        if (!$filename) {
            $filename = 'mysal-' . $tablename . $fileExt;
        }
        if ($isDownload) {
            HtmlHelper::setExportHeader($isExcel, $filename);
        }
        $row = $this->getTableInfo($dbname, $tablename);
        $columns = $this->getColumns($dbname, $tablename);
        $indexes = $this->getIndexes($dbname, $tablename);
        $content .= '<h1>数据表 ' . $tablename . '</h1><div>日期：' . date('Y-m-d H:i:s') . '</div>';
        $content .= '<section><h3>';
        $content .= $tablename . ' <small>';
        $content .= ($row['TABLE_COMMENT'] ? ('(' . $row['TABLE_COMMENT'] . ') ') : '');
        $content .= count($columns) . ' 个字段</small></h3>';
        if ($columns) {
            $content .= $tableBegin . '<tr><th>--</th><th>字段</th><th>数据类型</th><th>可空</th><th>默认值</th><th>索引键</th>'
                . '<th>备注</th></tr>';
            $i = 0;
            foreach ($columns as $col) {
                $i++;
                $content .= '<tr><td>' . $i . '</td><td>' . $col['COLUMN_NAME'] . '</td><td>'
                    . $col['COLUMN_TYPE'] . '</td><td>';
                $content .= $col['IS_NULLABLE'] . '</td><td>' . $col['COLUMN_DEFAULT'] . '</td><td>'
                    . $col['COLUMN_KEY'] . '</td><td>';
                $content .= SqlHelper::parseDbComment($col['COLUMN_COMMENT']) . '</td></tr>';
            }
            $content .= '</table>';
        }
        if ($indexes) {
            $content .= $tableBeginIndex . '<tr><th>索引名称</th><th>列名称</th></tr>';
            foreach ($indexes as $idx) {
                $content .= '<tr><td>' . $idx['Key_name'] . '</td><td>' . $idx['Column_name'] . '</td></tr>';
            }
            $content .= '</table>';
        }
        $content .= '</div></section>';
        $content .= '</body></html>';
        if ($isLocalFile) {
            file_put_contents($filename, $content);
        } else {
            echo $content;
            exit();
        }
    }

    /**
     * 导出数据表结构到 Phinx Migrate 迁移文件
     * @param string $dbname
     * @param string $tablename
     * @param string $framework
     * @param string $filename
     * @param boolean $overwrite
     */
    public function exportTableSchemaAsMigrateForCli(
        $dbname,
        $tablename,
        $framework = 'phinx',
        $filename = '',
        $overwrite = false
    ) {
        $option = ['isDownload' => false, 'overwrite' => $overwrite];
        $this->exportTableSchemaAsMigrate($dbname, $tablename, $option, $framework, $filename);
    }
    /**
     * 导出数据表结构到 Phinx Migrate 迁移文件
     * @param string $dbname
     * @param string $tablename
     * @param string $framework
     */
    public function exportTableSchemaAsMigrateForDownload($dbname, $tablename, $framework = 'phinx')
    {
        $option = ['isDownload' => true, 'overwrite' => false];
        $this->exportTableSchemaAsMigrate($dbname, $tablename, $option, $framework, '');
    }
    /**
     * 导出数据表结构到 Phinx Migrate 迁移文件
     * 如果不知道什么是Phinx可以参考百度一下cakephp Phinx
     * @param string $dbname 当前数据库
     * @param bool $tablename 当前数据表
     * @param array $option 参数选项
     * @param string $framework 框架名称
     * @version 2020-2-5
     */
    private function exportTableSchemaAsMigrate($dbname, $tablename, $option, $framework = 'phinx', $filename = '')
    {
        $defaultoption = ['isDownload' => true, 'overwrite' => true];
        extract(array_merge($defaultoption, $option));
        $dbname = $this->checkDbName($dbname);
        $classname = mysal_table2classname($tablename);
        if ($isDownload) {
            if (!$filename) {
                $filename = date('YmdHis') . '_' . $classname . '.php';
            }
            header("Content-type:application/octet-stream");
            header('Content-Disposition:attachment; filename=' . $filename);
        }
        // 排版变量
        $tab = "\n";
        $tabx2 = "\t\t";
        $content = '<?php' . PHP_EOL . PHP_EOL;
        // 每种框架头部都不一样
        switch ($framework) {
            case 'mysal':
                $content .= 'class ' . $classname . ' {' . PHP_EOL . PHP_EOL;
                $content .= $tab . 'protected $tablename=\'' . $tablename . '\';' . PHP_EOL . PHP_EOL;
                $content .= $tab . 'public function change($table) {' . PHP_EOL . PHP_EOL;
                break;
            default:
                $content .= 'use Phinx\Migration\AbstractMigration;' . PHP_EOL . PHP_EOL;
                $content .= 'class ' . $classname . ' extends AbstractMigration {' . PHP_EOL . PHP_EOL;
                $content .= $tab . 'protected $tablename=\'' . $tablename . '\';' . PHP_EOL . PHP_EOL;
                $content .= $tab . 'public function change() {' . PHP_EOL . PHP_EOL;
                break;
        }

        $tableinfo = $this->getTableInfo($dbname, $tablename);
        $columns = $this->getColumns($dbname, $tablename);
        $content .= $tabx2 . '$table=$this->table($this->tableName, [\'comment\'=>\''
            . SqlHelper::parseDbComment($tableinfo['TABLE_COMMENT']) . '\']);' . PHP_EOL;
        $isfirst = true;
        $datatype = '';
        $defaultval = '';
        $colname = '';
        $hascreatedat = false;
        foreach ($columns as $col) {
            $colname = $col['COLUMN_NAME'];
            // phinx 默认创建 id 主键列，所以无需在代码中定义 id 主键列
            if ('id' == $colname) {
                continue;
            }
            // 时间戳字段
            if ('created_at' == $colname) {
                $hascreatedat = true;
                $content .= "\t\t\t->addTimestamps()" . PHP_EOL;
                continue;
            }
            if ($hascreatedat && 'updated_at' == $colname) {
                $hascreatedat = false;
                continue;
            }
            // fastadmin 等框架的时间戳字段
            if ('createtime' == $colname) {
                $hascreatedat = true;
                $content .= "\t\t\t->addTimestamps('createtime','updatetime')" . PHP_EOL;
                continue;
            }
            if ($hascreatedat && 'updatetime' == $colname) {
                $hascreatedat = false;
                continue;
            }
            if ($isfirst) {
                $isfirst = false;
                $content .= $tabx2 . '$table->addColumn(\'';
            } else {
                $content .= "\t\t\t->addColumn('";
            }
            $datatype = SqlHelper::parseDbTypeToPhinxType($col['COLUMN_TYPE']);
            $defaultval = $col['COLUMN_DEFAULT'];
            $content .= $col['COLUMN_NAME'] . '\', \'' . $datatype['type'] . '\', [\'comment\'=>\''
                . SqlHelper::parseDbComment($col['COLUMN_COMMENT']) . '\'';
            if (isset($datatype['values'])) {
                $content .= ', \'values\'=>[' . $datatype['values'] . ']';
            }
            if ('YES' == $col['IS_NULLABLE']) {
                $content .= ', \'null\'=>true';
            }
            switch ($datatype['type']) {
                case 'string':
                    $content .= ', \'limit\'=>' . $col['CHARACTER_MAXIMUM_LENGTH'];
                    break;
                case 'float':
                case 'decimal':
                    $content .= ', \'precision\'=>' . $col['NUMERIC_PRECISION'];
                    $content .= ', \'scale\'=>' . $col['NUMERIC_SCALE'];
                    break;
            }
            if (!is_null($defaultval)) {
                $content .= ', \'default\'=>' . ($datatype['is_number'] ? $defaultval : "'$defaultval'");
            }
            $content .= '])' . PHP_EOL;
        }
        foreach ($columns as $col) {
            if ($col['COLUMN_KEY'] && 'id' != $col['COLUMN_NAME']) {
                $content .= "\t\t\t" . '->addIndex(\'' . $col['COLUMN_NAME'] . '\' . [\'name\'=>\'idx_'
                    . $col['COLUMN_NAME'] . '\'])' . PHP_EOL;
            }
        }
        $content .= "\t\t\t->create();" . PHP_EOL;
        $content .= "\t}" . PHP_EOL . '}' . PHP_EOL;
        if ($isDownload) {
            echo $content;
            exit();
        } else {
            file_put_contents($filename, $content);
        }
    }
    /**
     * 导出数据表结构到 Phinx Seed 测试数据文件
     * @param string $dbname
     * @param string $tablename
     * @param string $framework
     * @param string $filename
     * @param boolean $overwrite
     */
    public function exportTableSchemaAsSeedForCli(
        $dbname,
        $tablename,
        $framework = 'phinx',
        $filename = '',
        $overwrite = false
    ) {
        $option = ['isDownload' => false, 'overwrite' => $overwrite];
        $this->exportTableSchemaAsSeed($dbname, $tablename, $option, $framework, $filename);
    }
    /**
     * 导出数据表结构到 Phinx Seed 测试数据文件
     * @param string $dbname
     * @param string $tablename
     * @param string $framework
     */
    public function exportTableSchemaAsSeedForDownload($dbname, $tablename, $framework = 'phinx')
    {
        $option = ['isDownload' => true, 'overwrite' => false];
        $this->exportTableSchemaAsSeed($dbname, $tablename, $option, $framework, '');
    }
    /**
     * 导出数据表结构到 Phinx Seed 测试数据文件
     * @param string $dbname
     * @param string $tablename
     * @param array $option
     * @param string $framework
     * @param string $filename
     * @version 2020-2-5
     */
    private function exportTableSchemaAsSeed($dbname, $tablename, $option, $framework = 'phinx', $filename = '')
    {
        $defaultoption = ['isDownload' => true, 'overwrite' => true];
        extract(array_merge($defaultoption, $option));
        $dbname = $this->checkDbName($dbname);
        $tab = "\t";
        $tabx2 = "\t\t";
        $tabx3 = "\t\t\t";
        $classname = mysal_table2classname($tablename) . 'Seeder';
        if (!$filename) {
            $filename = $classname . '.php';
        }
        if ($isDownload) {
            header("Content-type:application/octet-stream");
            header('Content-Disposition:attachment; filename=' . $filename);
        }
        $comment = $this->getTableInfo($dbname, $tablename, 'TABLE_COMMENT');
        $content = '<?php' . PHP_EOL . PHP_EOL;
        switch ($framework) {
            case 'mysal':
                $content .= 'namespace mysal\data\seeds;' . PHP_EOL . PHP_EOL;
                $content .= '// ' . ($comment ? $comment : $tablename) . PHP_EOL;
                $content .= 'class ' . $classname . ' {' . PHP_EOL;
                $content .= $tab . 'public function run($table) {' . PHP_EOL;
                break;
            default:
                $content .= 'use Phinx\Seed\AbstractSeed;' . PHP_EOL . PHP_EOL;
                $content .= '// ' . ($comment ? $comment : $tablename) . PHP_EOL;
                $content .= 'class ' . $classname . ' extends AbstractSeed {' . PHP_EOL;
                $content .= $tab . 'public function run() {' . PHP_EOL;
                $content .= $tabx2 . '//$faker = Faker\Factory::create();' . PHP_EOL;
                break;
        }
        $columns = $this->getColumns($dbname, $tablename);
        $content .= $tabx2 . '$data = [];' . PHP_EOL;
        $content .= '{#customdata#}';
        $content .= $tabx2 . 'for ($i = 0; $i < 100; $i++) {' . PHP_EOL;
        $content .= $tabx3 . '$data[] = [' . PHP_EOL;
        $datatype = [];
        // $defaultval = '';
        $colname = '';
        $customdata = '';
        foreach ($columns as $col) {
            $colname = $col['COLUMN_NAME'];
            // $defaultval = $col['COLUMN_DEFAULT'];
            if ('id' == $colname || 'created_at' == $colname || 'updated_at' == $colname) {
                continue;
            }
            $datatype = SqlHelper::parseDbTypeToPhinxType($col['COLUMN_TYPE']);
            $content .= "\t\t\t\t'$colname' => ";
            switch ($datatype['type']) {
                case 'datetime':
                    $content .= 'date(\'Y-m-d H:i:s\')';
                    break;
                case 'date':
                    $content .= 'date(\'Y-m-d\')';
                    break;
                case 'time':
                    $content .= 'date(\'H:i:s\')';
                    break;
                case 'string':
                case 'text':
                    // echo is_null($defaultval) ? "'test ' . \$i" : "'$defaultval'";
                    if ('password' == $colname) {
                        $content .= 'md5(\'Aa123456\')';
                    } elseif (false !== strpos($colname, 'name')) {
                        $content .= "'{$colname}-' . \$i";
                    } else {
                        $content .= "'test-' . \$i";
                    }
                    break;
                case 'enum':
                case 'set':
                    $customdata .= $tabx2 . '$' . $colname . 'Data = [' . $datatype['values'] . '];' . PHP_EOL;
                    $content .= '$' . $colname . 'Data[array_rand($' . $colname . 'Data)]';
                    break;
                case 'year': // year(4)
                    $content .= 'date(\'Y\')';
                    break;
                default:
                    // echo is_null($defaultval) ? '0' : $defaultval;
                    $content .= strpos($colname, 'time') ? 'time()' : 'rand(1,100)';
                    break;
            }
            $content .= ',' . PHP_EOL;
        }
        $content = str_replace('{#customdata#}', $customdata, $content);
        $content .= $tabx3 . '];' . PHP_EOL;
        $content .= $tabx2 . '}' . PHP_EOL;
        if ('mysal' == $framework) {
            $content .= $tabx2 . '// $table->truncate(\'' . $tablename . '\');' . PHP_EOL;
            $content .= $tabx2 . 'return $table->insert(\'' . $tablename . '\', $data);' . PHP_EOL;
        } else {
            $content .= $tabx2 . '$this->insert(\'' . $tablename . '\', $data);' . PHP_EOL;
        }
        $content .= $tab . '}' . PHP_EOL . '}';
        if ($isDownload) {
            echo $content;
            exit();
        } else {
            if (!$overwrite && file_exists($filename)) {
                return false;
            }
            file_put_contents($filename, $content);
            return true;
        }
    }
    /**
     * 导出查询结果到 Json 文件
     * @param string $database
     * @param string $tablename
     * @param string $sqlstmt
     */
    public function exportQueryDataAsJsonForWeb($database, $tablename, $sqlstmt)
    {
        $option = ['isCli' => false, 'isDownload' => false];
        $this->exportQueryDataAsJson($database, $tablename, $sqlstmt, $option);
    }

    /**
     * 导出查询结果到 Json 文件
     * @param string $database
     * @param string $tablename
     * @param string $sqlstmt
     */
    public function exportQueryDataAsJsonForDownload($database, $tablename, $sqlstmt)
    {
        $option = ['isCli' => false, 'isDownload' => true];
        $this->exportQueryDataAsJson($database, $tablename, $sqlstmt, $option);
    }
    /**
     * 导出查询结果到 Json 文件
     * @param string $database
     * @param string $tablename
     * @param string $sqlstmt
     * @param string $filename
     */
    public function exportQueryDataAsJsonForCli($database, $tablename, $sqlstmt, $filename)
    {
        $option = ['isCli' => true, 'isDownload' => false, 'filename' => $filename];
        $this->exportQueryDataAsJson($database, $tablename, $sqlstmt, $option);
    }
    /**
     * 导出查询结果到 Json 文件
     * @param string $database
     * @param string $sqlstmt
     * @param array $option
     */
    public function exportQueryDataAsJson($database, $tablename, $sqlstmt, $option)
    {
        $defaultoption = ['isCli' => false, 'isDownload' => false, 'filename' => ''];
        extract(array_merge($defaultoption, $option));
        if (!$isCli && !$isDownload) {
            header('Content-Type:application/json; charset=utf-8');
        }
        if ($isDownload) {
            header("Content-type:application/octet-stream");
            header("Content-Disposition:attachment; filename=mysal-" . ($tablename ? $tablename : '') . ".json");
        }
        $this->checkDbName($database);
        $list = $this->query($sqlstmt);
        if ($isCli) {
            file_put_contents($filename, json_encode($list));
        } else {
            echo json_encode($list);
            exit();
        }
    }
    public function exportQueryDataAsMarkdownForCli($database, $tablename, $sqlstmt, $filename)
    {
        $option = ['isCli' => true, 'isDownload' => false, 'filename' => $filename];
        $this->exportQueryDataAsMarkdown($database, $tablename, $sqlstmt, $option);
    }
    public function exportQueryDataAsMarkdownForWeb($database, $tablename, $sqlstmt)
    {
        $option = ['isCli' => false, 'isDownload' => false, 'filename' => ''];
        $this->exportQueryDataAsMarkdown($database, $tablename, $sqlstmt, $option);
    }
    public function exportQueryDataAsMarkdownForDownload($database, $tablename, $sqlstmt)
    {
        $option = ['isCli' => false, 'isDownload' => true, 'filename' => ''];
        $this->exportQueryDataAsMarkdown($database, $tablename, $sqlstmt, $option);
    }
    /**
     * 导出查询结果到 Markdown 文件
     * @param string $database
     * @param string $tablename
     * @param string $sqlstmt
     * @param array $option
     * @version 2017-12-15
     */
    public function exportQueryDataAsMarkdown($database, $tablename, $sqlstmt, $option)
    {
        $defaultoption = ['isCli' => false, 'isDownload' => false, 'filename' => ''];
        extract(array_merge($defaultoption, $option));
        if ($isDownload) {
            if (!$filename) {
                $filename = 'mysal-' . ($tablename ? $tablename . '-' : '') . date('Ymd-His') . '.md';
            }
            header("Content-type:application/octet-stream");
            header("Content-Disposition:attachment; filename={$filename}");
        }
        $this->checkDbName($database);
        $ln = ($isCli || $isDownload) ? PHP_EOL : '<br />';
        $list = $this->query($sqlstmt);
        $content = '### 导出数据 ' . date('Ymd-His') . $ln . $ln;
        if ($tablename) {
            $content .= '对象：' . $tablename . $ln;
        }
        $content .= '数量：' . count($list) . $ln;
        $content .= '命令：' . str_replace('`', '\`', $sqlstmt) . $ln . $ln;
        if ($list) {
            $i = 0;
            foreach ($list as $row) {
                if ($i < 1) {
                    $content .= '| ' . implode(' | ', array_keys($row)) . ' |' . $ln;
                    $content .= '| ' . str_repeat('--- |', count($row)) . $ln;
                }
                $i++;
                $content .= '| ' . implode(" | ", array_values($row)) . ' |' . $ln;
            }
            unset($list);
        } else {
            $content .= '没有查询到任何记录!';
        }
        $content .= $ln . $ln;
        if ($isCli) {
            file_put_contents($filename, $content);
        } else {
            echo $content;
            exit();
        }
    }
    /**
     * 导出查询结果到 Excel 文件
     * @param string $database
     * @param string $tablename
     * @param string $sqlstmt
     */
    public function exportQueryDataAsHtmlOnline($database, $tablename, $sqlstmt)
    {
        $option = ['isCli' => false, 'isExcel' => false, 'isDownload' => false];
        $this->exportQueryDataAsExcel($database, $tablename, $sqlstmt, $option);
    }
    /**
     * 导出查询结果到 Excel 文件
     * @param string $database
     * @param string $tablename
     * @param string $sqlstmt
     */
    public function exportQueryDataAsHtmlForDownload($database, $tablename, $sqlstmt)
    {
        $option = ['isCli' => false, 'isExcel' => false, 'isDownload' => true];
        $this->exportQueryDataAsExcel($database, $tablename, $sqlstmt, $option);
    }
    /**
     * 导出查询结果到 Excel 文件
     * @param string $database
     * @param string $tablename
     * @param string $sqlstmt
     * @param string $filename
     */
    public function exportQueryDataAsHtmlForCli($database, $tablename, $sqlstmt, $filename)
    {
        $option = ['isCli' => true, 'isExcel' => false, 'isDownload' => false, 'filename' => $filename];
        $this->exportQueryDataAsExcel($database, $tablename, $sqlstmt, $option);
    }
    /**
     * 导出查询结果到 Excel 文件
     * @param string $database
     * @param string $tablename
     * @param string $sqlstmt
     */
    public function exportQueryDataAsExcelForDownload($database, $tablename, $sqlstmt)
    {
        $option = ['isCli' => false, 'isExcel' => true, 'isDownload' => true];
        $this->exportQueryDataAsExcel($database, $tablename, $sqlstmt, $option);
    }
    /**
     * 导出查询结果到 Excel 文件
     * @param string $database
     * @param string $tablename
     * @param string $sqlstmt
     * @param string $filename
     */
    public function exportQueryDataAsExcelForCli($database, $tablename, $sqlstmt, $filename)
    {
        $option = ['isCli' => true, 'isExcel' => true, 'isDownload' => false, 'filename' => $filename];
        $this->exportQueryDataAsExcel($database, $tablename, $sqlstmt, $option);
    }
    /**
     * 导出查询结果到 Excel 文件
     * @param string $database
     * @param string $tablename
     * @param string $sqlstmt
     * @param array $option
     * @version 2018-1-10
     */
    public function exportQueryDataAsExcel($database, $tablename, $sqlstmt, $option)
    {
        $defaultoption = ['isCli' => false, 'isExcel' => true, 'isDownload' => true, 'filename' => ''];
        extract(array_merge($defaultoption, $option));
        if ($isExcel) {
            header("Content-Type: application/vnd.ms-excel; name='excel'");
        }
        if ($isDownload) {
            if (!$filename) {
                $filename = 'mysal-' . ($tablename ? ($tablename . '-') : '') . date('Ymd-His')
                    . ($isExcel ? '.xls' : '.html');
            }
            header('Content-type:application/octet-stream');
            header('Content-Disposition:attachment; filename=' . $filename);
        }
        $this->checkDbName($database);
        $list = $this->query($sqlstmt);
        if ($list) {
            if ($isExcel) {
                $content = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-'
                    . 'microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta http-equiv='
                    . 'Content-Type content="text/html; charset=utf-8"></head><body>';
                $tableBegin = '<table border="1">';
            } else {
                $content = '<html><head><title>导出数据 ' . $tablename . '</title>';
                $content .= '<style>body{font-size:13px;}h1{font-size:18px;margin:5px 0;}.codearea{margin-bottom:'
                    . '10px;}code{display:block;background-color:#384548;color:#abe338;max-width:900px;max-height:'
                    . '150px;overflow:auto;padding:5px;line-height:18x;}td,th{font-size:13px;}.grid{border:solid 1px '
                    . '#CCC;border-bottom:none;border-right:none;}.grid tr:hover{background-color:#efefef;}.grid '
                    . 'tr.active{background-color:#fc0;}.grid th,.grid td{border:solid 1px #CCC;border-left:none;'
                    . 'border-top:none;padding:5px;}.grid th{word-break:keep-all;}.grid th a{font-weight:normal;'
                    . 'margin-left:3px;}.bluegrid tr:nth-child(even){background:#e6e6e6;}.bluegrid th{background:'
                    . 'rgb(81,130,187);color:#FFF;}.bluegrid th a{color:#FFF;}</style></head><body>';
                $tableBegin = '<table cellpadding="0" cellspacing="0" class="grid bluegrid">';
            }
            $content .= '<h1>导出数据 ' . $tablename . '</h1>';
            $content .= '<div class="codearea">时间：' . date('Y-m-d H:i:s') . '<br />SQL 命令：<code>select '
                . '`condition`, `createtime`, `icon`, `id`, `ismenu`, `name`, `pid`, `remark`, `status`, `title`, '
                . '`type`, `updatetime`, `weigh` from fa_auth_rule order by `condition` desc limit 50;</code></div>'
                . $tableBegin;
            $i = 0;
            foreach ($list as $row) {
                if ($i < 1) {
                    $content .= '<tr><th>--</th><th>' . implode('</th><th>', array_keys($row)) . '</th></tr>';
                }
                $i++;
                $content .= '<tr><td>' . $i . '</td><td>' . implode('</td><td>', array_values($row)) . '</td></tr>';
            }
            unset($list);
            $content .= '</table></body></html>';
        } else {
            $content = '没有查询到任何记录!';
        }
        if ($isCli) {
            file_put_contents($filename, $content);
        } else {
            echo $content;
            exit();
        }
    }

    /**
     * 导出查询结果到 SQL 脚本
     * @param string $dbname
     * @param string $tablename
     * @param string $sqlstmt
     * @param string $filename
     */
    public function exportQueryDataAsSqlForCli($dbname, $tablename, $sqlstmt, $filename)
    {
        $this->exportQueryDataAsSql($dbname, $tablename, $sqlstmt, false, $filename);
    }
    /**
     * 导出查询结果到 SQL 脚本
     * @param string $dbname
     * @param string $tablename
     * @param string $sqlstmt
     */
    public function exportQueryDataAsSqlForDownload($dbname, $tablename, $sqlstmt)
    {
        $this->exportQueryDataAsSql($dbname, $tablename, $sqlstmt, true);
    }
    /**
     * 导出查询结果到 SQL 脚本
     * @param string $dbname
     * @param string $tablename
     * @param string $sqlstmt
     * @param boolean $isDownload
     * @param string $filename
     */
    public function exportQueryDataAsSql($dbname, $tablename, $sqlstmt, $isDownload = true, $filename = '')
    {
        if (!$filename) {
            $filename = 'mysal-' . $tablename . '-' . date('Ymd-His') . '.sql';
        }
        if ($isDownload) {
            header('Content-type:application/octet-stream');
            header('Content-Disposition:attachment; filename=' . $filename);
        }
        $this->checkDbName($dbname);
        // COLUMN_NAME, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE,
        // EXTRA, COLUMN_COMMENT
        $columns = $this->getColumns($dbname, $tablename);
        $fields = [];
        if ($columns) {
            foreach ($columns as $row) {
                $fields[$row['COLUMN_NAME']] = $row;
            }
        }
        try {
            $list = $this->query($sqlstmt);
        } catch (\PDOException $ex) {
            echo $ex->getMessage();
            return false;
        }
        $content = '/* ' . PHP_EOL . '数据脚本';
        $content .= '对象：' . $tablename;
        $content .= '数量：' . count($list) . PHP_EOL;
        $content .= '时间：' . date('Y-m-d H:i:s') . PHP_EOL;
        $content .= '命令：' . $sqlstmt . PHP_EOL;
        $content .= '*/' . PHP_EOL . PHP_EOL;

        if (!$list) {
            echo '命令：', $sqlstmt, ' 没有查询到任何记录! ', PHP_EOL;
            return false;
        }
        $i = 0;
        $strfields = '';
        $values = [];
        $column = [];
        $numbericTypes = ['int', 'smallint', 'bigint', 'decimal', 'float'];
        foreach ($list as $row) {
            if ($i < 1) {
                $strfields = implode('`,`', array_keys($row));
            }
            $values = [];
            foreach ($row as $field => $value) {
                $column = $fields[$field];
                if (is_null($value)) {
                    $values[] = 'null';
                } elseif (in_array($column['DATA_TYPE'], $numbericTypes)) {
                    $values[] = $value;
                } else {
                    $values[] = "'$value'";
                }
            }
            $content .= 'insert into ' . $tablename . ' (`' . $strfields . '`) values (' . implode(",", $values) . ');' . PHP_EOL;
            $i++;
        }
        if ($isDownload) {
            echo $content;
            exit();
        } else {
            file_put_contents($filename, $content);
            return true;
        }
    }

    /**
     * 显示执行结果
     * @param array $sqlstmts
     * @param string $envName
     * @param string $dbname
     * @param string $tablename
     */
    public function showQueryResultForCli($sqlstmts, $envName = '', $dbname = '', $tablename = '')
    {
        $iscli = true;
        $this->showQueryResult($sqlstmts, compact('envName', 'dbname', 'tablename', 'iscli'));
    }
    /**
     * 显示执行结果
     * @param array $sqlstmts
     * @param string $envName
     * @param string $dbname
     * @param string $tablename
     * @param string $token
     */
    public function showQueryResultForWeb($sqlstmts, $envName = '', $dbname = '', $tablename = '', $token = '')
    {
        $iscli = false;
        $this->showQueryResult($sqlstmts, compact('envName', 'dbname', 'tablename', 'token', 'iscli'));
    }
    /**
     * 显示执行结果
     * @param string $sqlstmt SQL 命令
     * @param array $option 参数选项
     */
    private function showQueryResult($sqlstmts, $option)
    {
        $defaultOption = [
            'envName' => '',
            'dbname' => '',
            'tablename' => '',
            'token' => '',
            'iscli' => false,
        ];
        extract(array_merge($defaultOption, $option));
        if (!$sqlstmts) {
            return;
        }
        $this->checkDbName($dbname);
        $sqlstmtcount = 0; // 有效命令数量
        foreach ($sqlstmts as $sqlstmtone) {
            if (!$sqlstmtone) {
                continue; // 跳过空语句
            }
            $sqlstmtone = trim($sqlstmtone); // 过滤首尾空格
            $whitespacePos = strpos($sqlstmtone, ' ');
            if (false === $whitespacePos) {
                $sqlstmtprefix = $sqlstmtone;
            } else {
                $sqlstmtprefix = trim(strtolower(substr($sqlstmtone, 0, $whitespacePos))); // 命令前缀
            }
            if ($sqlstmtprefix == 'eg.') {
                continue; // 过滤eg.示例前缀
            }
            if ($tablename) { // 替换语句中的 {#object} 变量，省得表名很长的时候要写很累 2017-12-15
                $sqlstmtone = str_replace('{#object}', $tablename, $sqlstmtone);
            }
            $sqlstmtcount++;
            switch ($sqlstmtprefix) {
                case 'select':
                case 'show':
                case 'desc':
                    if ($iscli) {
                        echo '# ', $sqlstmtcount, ' [', mysal_short_text($sqlstmtone, 100), '] ';
                    } else {
                        $url = '?env=' . $envName . '&db=' . $dbname . '&table=' . $tablename . '&sqlstmt='
                            . urlencode($sqlstmtone) . '&token=' . $token;
                        echo '<div><div class="btn-group btn-group-sm" title="第', $sqlstmtcount, '条命令">';
                        echo '<a class="btn lnk-switch" href="#" title="展开或收起详情内容">&equiv;</a>';
                        echo '<a class="btn text-info" disabled style="font-size:15px">查询结果 (#', $sqlstmtcount,
                            ')</a>';
                        echo '<a class="btn btn-light" href="', $url, '&action=exportmd" target="_blank" title="导出',
                            '结果集到 Markdown 文件">导出 MD</a>';
                        echo '<a class="btn btn-light" href="', $url, '&action=exportexcel" target="_blank" title="导出',
                            '结果集到 Excel 文件">导出 Excel</a>';
                        echo '<a class="btn btn-light" href="', $url, '&action=exporthtml" target="_blank" title="导出',
                            '结果集到HTML文件">导出 HTML</a>';
                        echo '<a class="btn btn-light" href="', $url, '&action=exportjson" target="_blank" title="导出',
                            '结果集到 JSON 文件">导出 JSON</a>';
                        echo '<a class="btn btn-light" href="', $url, '&action=exportjsononline" target="_blank" title="导出',
                            '结果集到 JSON 文件">在线 JSON</a>';
                        if ($dbname && $tablename) {
                            echo '<a class="btn btn-light" href="', $url, '&action=exportsql" target="_blank" title="',
                                '导出结果集到SQL文件">导出 SQL</a>';
                            echo '<a class="btn btn-light" href="', $url, '&action=exporthtmlonline" target="_blank" ',
                                'title="在新页面显示结果集">新页面查看</a>';
                        }
                        echo '</div>';
                        echo '<div class="fieldset_content">';
                        echo '<div class="code-editor" style="padding:5px"><code>', $sqlstmtone, '</code></div>';
                    }
                    try {
                        $list = $this->query($sqlstmtone);
                        if ($iscli) {
                            echo '共查询到 ', count($list), ' 条记录', PHP_EOL;
                            HtmlHelper::showTableCli($list, false, true);
                        } else {
                            HtmlHelper::showTableWeb($list);
                        }
                        if ($list) {
                            unset($list);
                        }
                    } catch (\Exception $ex) {
                        if ($iscli) {
                            echo $ex->getMessage();
                        } else {
                            echo '<p>', $ex->getMessage(), '</p>';
                        }
                    }
                    if (!$iscli) {
                        echo '</div>';
                        echo '</div>';
                    }
                    break;
                case 'insert':
                case 'update':
                case 'delete':
                case 'truncate':
                case 'alter':
                case 'create':
                case 'drop':
                case 'grant': // 更改数据库用户授权
                case 'revoke': // 撤销用户权限
                case 'flush': // flush privileges;
                case 'set': // 更改或设置用户密码
                    if (!$iscli) {
                        echo '<fieldset>';
                        echo '<legendtitle="第', $sqlstmtcount, '条命令">执行结果 (#', $sqlstmtcount, ')';
                        echo '<div class="code-editor" style="padding:5px"><code>', $sqlstmtone, '</code></div>';
                    }
                    try {
                        $result = $this->execute($sqlstmtone);
                        if ($iscli) {
                            echo '# ', $sqlstmtcount, ' [', mysal_short_text($sqlstmtone, 30), '] 执行结果影响 ';
                            echo $result, ' 行! ', PHP_EOL;
                        } else {
                            $url = '?env=' . $envName;
                            if ($dbname) {
                                $url .= '&db=' . $dbname;
                            }
                            if ($tablename) {
                                $url .= '&table=' . $tablename;
                            }
                            $url .= '&token=' . $token;
                            echo '<p style="margin:2px 0">执行结果影响 ', $result, ' 行! ';
                            echo '<a class="btn btn-sm btn-outline-secondary" href="', $url, '" ',
                                'style="padding:0 0.5rem">刷新</a></p>'; // 刷新侧边栏
                        }
                    } catch (\Exception $ex) {
                        if ($iscli) {
                            echo '# ', $sqlstmtcount, ' 命令执行错误：', PHP_EOL, $ex->getMessage(), PHP_EOL;
                            echo '[', mysal_short_text($sqlstmtone, 120), ']', PHP_EOL, PHP_EOL;
                        } else {
                            echo '<p><span class="text-danger">命令执行错误：</span><br />', $ex->getMessage(), '</p>';
                        }
                    }
                    if (!$iscli) {
                        echo '</fieldset>';
                    }
                    break;
                default:
                    if ($iscli) {
                        echo '# ', $sqlstmtcount, ' 不支持的命令：', $sqlstmtone;
                    } else {
                        echo '<p><strong style="color:#F00">不支持的命令</strong>：', $sqlstmtone, '</p>';
                    }
                    break;
            }
        }
    }

    /**
     * 解析 sql 语句中的首个对象名称
     * @param string $sqlstmt
     * @return array [object, type]
     * @version 2017-12-20
     */
    public static function getObjectBySqlscript($sqlstmt)
    {
        $return = array('object' => '', 'type' => '');
        if (!$sqlstmt) {
            return $return;
        }
        $sqlstmt = trim($sqlstmt);
        if (false === strpos($sqlstmt, ' ')) {
            return $return;
        }
        $count = 0;
        $firstword = substr($sqlstmt, 0, strpos($sqlstmt, ' '));
        switch ($firstword) {
            case 'select': // select ... from ($table|$view) | select $func
                $count = preg_match('/ from[\s]+([^\s|^\(]+)/', $sqlstmt, $matches);
                if (!$count && false === strpos($sqlstmt, ' from ')) {
                    $return['type'] = 'function';
                    $count = preg_match('/select[\s]+([^\s|^\(]+)/', $sqlstmt, $matches);
                }
                break;
            case 'delete': // delete from $table
                $count = preg_match('/ from[\s]+([^\s]+)/', $sqlstmt, $matches);
                break;
            case 'insert': // insert into $table
                $count = preg_match('/ into[\s]+([^\s]+)/', $sqlstmt, $matches);
                break;
            case 'update': // update $table
                $count = preg_match('/update[\s]+([^\s]+)/', $sqlstmt, $matches);
                break;
            case 'alter': // alter table $table
                $count = preg_match('/ table[\s]+([^\s]+)/', $sqlstmt, $matches);
                break;
            case 'call': // call $proc
                $count = preg_match('/call[\s]+([^\s|^\(]+)/', $sqlstmt, $matches);
                $return['type'] = 'procedure';
                break;
        }
        if ($count) {
            $return['object'] = $matches[1];
            if (!$return['type']) {
                $return['type'] = 'table';
            }
        }
        return $return;
    }
}

/**
 * 服务器配置通用类
 * @version 2017-12-19 Added.
 */
class ConfigHelper
{
    /**
     * 加载外部配置文件
     * 如果外部配置文件存在，则外部配置文件会覆盖内置配置文件
     * @param array &$config
     * @param string $file 外部配置文件路径
     */
    public static function loadExtConfig(&$config, $file = './mysal/config/config.php')
    {
        if (!file_exists($file)) {
            return;
        }
        $exconfig = require $file;
        if (!is_array($exconfig)) {
            return;
        }
        // 数组合并只是简单粗暴地合并第一维键值，所以需要额外处理第二维和第三维的数据
        mysal_array_migrate($config, $exconfig);
        $config = array_merge($config, $exconfig);
    }
    /**
     * 获取配置内容中的当前数据库服务器配置
     * @param array $config
     * @param string $curenv 当前服务器名称
     * @param boolean $isCli 是否Cli环境
     * @return array
     * @version 2017-12-15
     */
    public static function getCurrentEnv($config, $curenv, $isCli = false)
    {
        if (!$curenv || empty($config[ENVIRONMENTS][$curenv])) {
            echo '数据库服务器环境 [' . $curenv . '] 无效！';
            if (isset($config[ENVIRONMENTS][DEFAULT_DATABASE])) {
                unset($config[ENVIRONMENTS][DEFAULT_DATABASE]);
            }
            $envs = array_keys($config[ENVIRONMENTS]);
            if ($envs) {
                if ($isCli) {
                    echo PHP_EOL, '以下服务器环境可以正常使用：', PHP_EOL;
                    echo implode(', ', $envs);
                } else {
                    global $token;
                    echo '<br />以下服务器环境可以正常使用：';
                    foreach ($envs as $envName) {
                        echo '<a href="?token=', $token, '&env=', $envName, '">', $envName, '</a> ';
                    }
                }
            } else {
                echo '（未找到可用的数据库环境配置信息，请检查配置文件）';
            }
            exit();
        }
        return $config[ENVIRONMENTS][$curenv]; // 当前数据库服务器配置
    }

    /**
     * 解析端口转发配置文件内容并融合到当前环境节点
     * @param array $config
     * @return array
     */
    private static function mergeRinetd($config)
    {
        if (!isset($config[RINETD])) {
            return $config;
        }
        $rinetd = $config[RINETD];
        // 去除扩展标签，规范配置内容
        unset($config[RINETD]);

        if (!is_array($rinetd) || !empty($rinetd)) {
            return $config;
        }
        if (!empty($rinetd['host'])) {
            // 返回服务器 IP，参考：SERVER_NAME 和HTTP_HOST的区别 (http://blog.sina.com.cn/s/blog_6d96d3160100q39x.html)
            $host_ip = gethostbyname($_SERVER['SERVER_NAME']);
            // 如果服务器 IP 和端口映射服务器一样，则自动忽略端口映射配置，提高性能 2017-10-13
            if ($host_ip == $rinetd['host']) {
            } else {
                if (!empty($rinetd['port'])) {
                    $config['port'] = $rinetd['port'];
                }
            }
        }
        return $config;
    }

    /**
     * 解析外部配置文件内容并融合到当前环境节点
     * @param array $config
     * @return array
     * @version 2017-12-6
     */
    public static function mergeExtFile(array $config)
    {
        if (!isset($config[EXTFILE]['path']) || !file_exists($config[EXTFILE]['path'])) {
            return $config;
        }
        $extconf = $config[EXTFILE];
        // 去除扩展标签，规范配置内容
        unset($config[EXTFILE]);

        // 字段映射配置 官方名称=>外部配置名称
        $map = isset($extconf['map']) ? $extconf['map'] : [];
        $filepath = $extconf['path'];
        // 判断外部配置文件的文件格式
        if ('php' == pathinfo($filepath, PATHINFO_EXTENSION)) {
            $content = include $filepath;
        } else {
            // txt类型的文件，比如 .env,.conf
            $content = [];
            $lines = file($filepath);
            // 配置内容读取的起始行
            $linestart = '';
            $isSkip = false;
            if (!empty($map['startline'])) {
                $linestart = trim($map['startline']);
                if ($linestart) {
                    $isSkip = true;
                }
            }
            foreach ($lines as $line) {
                $line = trim($line);
                if ($isSkip) {
                    if ($line = $linestart) {
                        $isSkip = false;
                    }
                    continue;
                }
                if (!strpos($line, '=') || $isSkip) {
                    continue;
                }
                list($name, $value) = array_map('trim', explode('=', $line, 2));
                $content[$name] = $value;
            }
        }
        // 判断外部配置内容是否为空
        if (!$content) {
            return $config;
        }
        if ($map) {
            $outdata = [];
            foreach ($map as $orginame => $extname) {
                if (array_key_exists($extname, $content)) {
                    $outdata[$orginame] = $content[$extname];
                }
            }
        } else {
            $outdata = $content;
        }
        return array_merge($config, $outdata);
    }

    /**
     * 规范化环境配置内容
     */
    public static function parseEnvironments($environments)
    {
        $outdata = [];
        foreach ($environments as $name => $data) {
            if (!$data) {
                continue;
            }
            // 只有 default_database 节点的值是字符串类型，其余都是环境节点的数组配置内容
            if (!is_array($data)) {
                $outdata[$name] = $data;
                continue;
            }
            // 读取外部配置内容
            if (isset($data[EXTFILE]['path']) && (!isset($data[EXTFILE]['enable']) || $data[EXTFILE]['enable'])) {
                $data = self::mergeExtFile($data);
            }
            // 如果节点配置没有设置密码内容则视为无效配置
            if (empty($data['password'])) {
                continue;
            }
            $outdata[$name] = self::mergeRinetd($data);
        }
        return $outdata;
    }
}
/**
 * 命令行助手类
 * @since 2020-02-13
 */
class ConsoleHelper
{
    const MYSAL_ROOT_DIR = './mysqladminlite/';
    const MYSAL_CONFIG_PATH = self::MYSAL_ROOT_DIR . 'config/';
    const MYSAL_EXPORTS_PATH = self::MYSAL_ROOT_DIR . 'exports/';
    const MYSAL_FUNCTIONS_PATH = self::MYSAL_ROOT_DIR . 'functions/';
    const MYSAL_MIGRATIONS_PATH = self::MYSAL_ROOT_DIR . 'migrations/';
    const MYSAL_SEEDS_PATH = self::MYSAL_ROOT_DIR . 'seeds/';
    const MYSAL_SQLS_PATH = self::MYSAL_ROOT_DIR . 'sqls/';

    /**
     * 命令行参数
     * @var array
     */
    protected $params = [];

    /**
     * 返回命令行参数列表
     * @param array $excludeFields 排除的字段
     * @param array $allowFields 允许的字段
     * @return array
     */
    protected function getParams(array $excludeFields = [], array $allowFields = [])
    {
        $params = $this->params;
        if ($allowFields) {
            $data = [];
            foreach ($allowFields as $key) {
                if (isset($params[$key])) {
                    $data[$key] = $params[$key];
                }
            }
            return $data;
        }
        if ($excludeFields) {
            foreach ($excludeFields as $key) {
                if (isset($params[$key])) {
                    unset($params[$key]);
                }
            }
        }
        return $params;
    }
    /**
     * 返回数组中指定键名的值
     * @param string|array $keys
     * @param array $params
     * @param mixed $defv
     * @return mixed
     */
    protected function getParam($keys, $defv = '')
    {
        if (!($params = $this->params)) {
            return $defv;
        }
        if (is_array($keys)) {
            foreach ($keys as $key) {
                if (isset($params[$key])) {
                    return trim($params[$key]);
                }
            }
            return $defv;
        } else {
            return isset($params[$keys]) ? trim($params[$keys]) : $defv;
        }
    }
    /**
     * 判断参数是否存在
     */
    protected function hasParam($name)
    {
        if (!($params = $this->params)) {
            return false;
        }
        return array_key_exists($name, $params);
    }
    /**
     * 返回命令行参数
     */
    protected function getCliParams($argc, $argv)
    {
        if ($argc < 3) {
            return false;
        }
        $pkeys = [];
        $pvals = [];
        foreach ($argv as $key => $value) {
            if ($key < 2) {
                continue;
            }
            if ($key % 2 < 1) {
                $pkeys[] = $value;
            } else {
                $pvals[] = $value;
            }
        }
        // 最后一个参数值忘记填写则默认为空值
        if (count($pkeys) != count($pvals)) {
            $pvals[] = '';
        }
        return array_combine($pkeys, $pvals);
    }
    /**
     * 交互式获取参数
     * @param string $msg 提示消息
     * @return mixed 用户输入的内容
     */
    protected function stdin($msg)
    {
        fwrite(STDOUT, $msg);
        return trim(fgets(STDIN));
    }
    /**
     * 返回命令的帮助内容
     */
    protected function getCmdHelp($cmdname = '')
    {
        $helps = [
            'help' => '--help [--output y]  : 显示帮助文档',
            'init' => 'init [--force y]  : 初始化',
            'status' => 'status [--env development]  : 查看数据库连接状态',
            'serv' => 'serv --host 127.0.0.1 --port 8866  : 启动网站服务器',
            'db:select' => 'db:select [--env development] --sql "select * from tbl_example limit 10;"  : ' .
                '显示数据库查询结果',
            'db:exec' => 'db:exec [--env development] --sql "update tbl_example set flag=1 where id=1;"  : 执行数据库'
                . '命令',
            'db:status' => 'db:status [--env development]  : 显示数据库状态',
            'db:variables' => 'db:variables [--env development]  : 显示数据库全局变量',
            'export:database' => 'export:database [--env development] [--database dbsample]  : 导出数据库，用于数据库'
                . '迁移',
            'export:data' => 'export:data  [--env development] --table tbl_example [--sql "select * from tbl"] '
                . '[--filetype excel|html|markdown|json|sql] : 导出数据，json 格式文件用于 mock 数据',
            'export:schema' => 'export:schema [--env development] [--table tbl_example] [--database dbsample2] '
                . '[--filetype excel|sql|html] [--eachfile y] [--prefix tbl_]  : 导出数据库结构数据',
            'source' => 'source [--env development] [--database dbsample] --file tbl_example.sql  ： 导入本地 sql 文件',
            'source:all' => 'source:all [--env development] --dir dbexample [--prefix tbl_] ： 批量导入本地 sql 文件',
            'template:sql' => 'template:sql --operate create_table|alter_table|create_view|create_routine '
                . '--file tbl_test.sql  : 创建数据库命令模板文件',
            'stub:crud' => 'stub:crud --table fa_test --project fastlara/admin --menu y --route y --force y  : 快速生成 CRUD 对应'
                . '的 MVC 文件、静态资源文件和路由配置',
            'stub:crud --mapfile' => 'stub:crud --table fa_test --project fastlara/admin --mapfile y --force y  : 生成 '
                . 'CRUD 字段与组件的映射关系的配置文件',
            'stub:crud --remove' => 'stub:crud --table fa_test --project fastlara/admin --menu y --force y --remove '
                . 'y  : 删除 CRUD 对应的 MVC 文件和静态资源文件',
            'stub:make' => 'stub:make --name controller:MyPackage/Sample --project fastlara/admin --force y  : 创建'
                . '存根模板文件',
            'stub:remove' => 'stub:remove --name controller:MyPackage/Sample --project fastlara/admin  : 删除存根模板'
                . '文件',
            'fnx:create' => 'fnx:create --file myfnx/fnsample.php  : 创建外部扩展功能函数模板文件',
            'fnx' => 'fnx --file myfnx/fnsample.php  : 运行外部扩展功能函数',
            'fastsql' => '[TODO]fastsql [--env development] --name tbl_example.simple  : 数据表快捷命令查询',
            'seed:create' => 'seed:create [--env development] [--file ExampleTableSeeder] '
                . '--table tbl_example  : 从数据表创建测试数据文件',
            'seed:autocreate' => 'seed:autocreate [--env development] [--prefix tbl_] [--force y] [--eltcount 10] '
                . '[--dir dbsample]  : 自动从数据库查找表名包含 "tbl_" 且记录数量少于 10 的数据表并生成数据种子文件',
            'seed:run' => 'seed:run [--env development] --file ExampleTableSeeder  : 运行测试数据生成文件',
            'seed:autorun' => 'seed:autorun [--env development] [--database dbsample2] --dir ac200216 [--prefix tbl_] '
                . '[--eltcount 0]  : 自动检测数据表，如果数据表是空的且本地有对应的 Seeder 文件，则自动运行测试数据填充',
            'migrate:create' => '[TODO]migrate:create [--env development] [--file ExampleTable --table tbl_example] '
                . '[--prefix tbl_] --version v1b20200212  : 从数据表创建数据迁移文件',
            'migrate:exportsql' => '[TODO]migrate:exportsql [--file ExampleTable] [--prefix Tbl] '
                . '--version v1b20200212  : 从数据表创建数据迁移文件',
            'migrate' => '[TODO]migrate [--env development] --file ExampleTable --version v1b20200213  : 运行数据迁移'
                . '文件',
            'migrate:rollback' => '[TODO]migrate:rollback  [--env development] --file ExampleTable '
                . '--version v1b20200212  : 运行数据迁移的回滚操作',

        ];
        return ($cmdname && isset($helps[$cmdname])) ? $helps[$cmdname] : $helps;
    }
    /**
     * 显示帮助文档
     */
    protected function execCmdHelp()
    {
        echo '输入命令 [--help --output y] 可以将帮助文档保存到本地文件。', PHP_EOL, PHP_EOL;
        $content = '目录结构：' . PHP_EOL
            . 'sal  : 初始化后自动生成的应用程序根目录' . PHP_EOL
            . '-- exports  : 导出文件目录' . PHP_EOL
            . '-- functions  : 外部功能函数目录' . PHP_EOL
            . '-- migrations  : 数据库迁移文件目录' . PHP_EOL
            . '-- seeds  : 数据种子文件目录' . PHP_EOL
            . '-- sqls  : SQL 脚本文件目录' . PHP_EOL
            . '-- stubs  : Stub 存根模板文件目录' . PHP_EOL
            . '-- config.php  : 外部配置文件' . PHP_EOL . PHP_EOL
            . '命令示例：' . PHP_EOL;
        $cmdHelps = $this->getCmdHelp();
        foreach ($cmdHelps as $cmdstr) {
            $content .= $cmdstr . PHP_EOL;
        }
        echo $content;
        $output = $this->getParam('--output');
        if ('y' == strtolower($output)) {
            try {
                $filename = self::MYSAL_EXPORTS_PATH . 'help.txt';
                file_put_contents($filename, $content);
                echo PHP_EOL, '帮助文档保存成功！', PHP_EOL, '路径：', $filename;
            } catch (\Exception $ex) {
                echo $ex->getMessage();
            }
        }
    }
    /**
     * 执行初始化命令
     * @param string MYSAL_ROOT_DIR
     * @param string $configfile
     */
    protected function execCmdInit($configfile)
    {
        // 排版变量
        $tab = "\t";
        $tab2 = "\t\t";
        $tab3 = "\t\t\t";
        $tab4 = "\t\t\t\t";
        $tab5 = "\t\t\t\t\t";
        $ln = PHP_EOL;
        try {
            if (mysal_ensure_dir(self::MYSAL_ROOT_DIR)) {
                mysal_ensure_dir(self::MYSAL_CONFIG_PATH);
                mysal_ensure_dir(self::MYSAL_EXPORTS_PATH);
                mysal_ensure_dir(self::MYSAL_MIGRATIONS_PATH);
                mysal_ensure_dir(self::MYSAL_SEEDS_PATH);
                mysal_ensure_dir(self::MYSAL_SQLS_PATH);
                mysal_ensure_dir(self::MYSAL_STUBS_PATH);
            }
            // 生成配置文件
            if (!file_exists($configfile) || ('y' == $this->getParam('-f'))) {
                $cfgcontent = "<?php{$ln}{$ln}/**{$ln} * phpmysqladminlite 扩展配置文件{$ln} */{$ln}return [{$ln}{$tab}"
                    . "// 数据迁移框架(目前支持 phinx 和 mysal 两种){$ln}{$tab}'migration_framework' => 'mysal',{$ln}"
                    . "{$tab}// 路径配置{$ln}{$tab}'paths' => [{$ln}{$tab2}'exports' => './mysal/data/exports/',{$ln}"
                    . "{$tab2}'functions' => './mysal/extensions/functions/',{$ln}{$tab2}'migrations' => './mysal/data/migrations/',"
                    . "{$ln}{$tab2}'seeds' => './mysal/data/seeds/',{$ln}{$tab2}'sqls' => './mysal/data/sqls/'{$ln}{$tab}],{$ln}"
                    . "{$tab}// 环境配置{$ln}{$tab}'environments' => [{$ln}{$tab2}'default_database' => 'development',"
                    . "{$ln}{$tab2}// 开发环境{$ln}{$tab2}'development' => [],{$ln}{$tab2}// 测试环境{$ln}{$tab2}"
                    . "'testing' => [{$ln}{$tab3}'adapter' => 'mysql',{$ln}{$tab3}'host' => '127.0.0.1',{$ln}{$tab3}"
                    . "'database' => 'dbsample',{$ln}{$tab3}'user' => 'root',{$ln}{$tab3}'password' => '',{$ln}{$tab3}"
                    . "'port' => 3306,{$ln}{$tab3}'charset' => 'utf8',{$ln}{$tab3}'extfile' => [],{$ln} {$tab2}],{$ln} "
                    . "{$tab2}// 生产环境{$ln} {$tab2}'production' => [],{$ln} {$tab2}// 基于 laravel 框架的第三方应用程"
                    . "序环境（vendorapp）{$ln} {$tab2}'laravelapp' => [{$ln} {$tab3}// laravel 项目的 .env 文件中没有 "
                    . "charset 配置项，因此这里要补充该配置项{$ln} {$tab3}'charset' => 'utf8mb4',{$ln} {$tab3}'extfile' "
                    . "=> [{$ln} {$tab4}'enable' => true,{$ln} {$tab4}'path' => '../.env',{$ln} {$tab4}// map 为字段"
                    . "映射，外部配置会覆盖内部配置，startline 为文本配置文件的有效起始行内容{$ln} {$tab4}'map' => [{$ln} "
                    . "{$tab5}'startline' => 'DB_CONNECTION=mysql',{$ln} {$tab5}'host' => 'DB_HOST', 'user' => "
                    . "'DB_USERNAME', 'password' => 'DB_PASSWORD', 'port' => 'DB_PORT',{$ln} {$tab5}'database' => "
                    . "'DB_DATABASE',{$ln} {$tab4}]{$ln} {$tab3}],{$ln} {$tab2}],{$ln} {$tab2}// 基于 thinkphp 框架的"
                    . "第三方应用程序环境（vendorapp）{$ln} {$tab2}'thinkphpapp' => [{$ln} {$tab3}// thinkphp 项目的 "
                    . ".env 文件中没有 charset 配置项，因此这里要补充该配置项{$ln} {$tab3}'charset' => 'utf8',{$ln} "
                    . "{$tab3}'extfile' => [{$ln} {$tab4}'enable' => true,{$ln} {$tab4}'path' => './mysal/config/"
                    . "thinkphp.env',{$ln} {$tab4}// map 为字段映射，外部配置会覆盖内部配置，startline 为文本配置文件的"
                    . "有效起始行内容{$ln} {$tab4}'map' => [{$ln} {$tab5}'startline' => '[database]',{$ln} {$tab5}"
                    . "'host' => 'hostname', 'user' => 'username', 'password' => 'password', 'port' => 'hostport',"
                    . "{$ln} {$tab5}'database' => 'database', 'prefix' => 'prefix'{$ln} {$tab4}]{$ln} {$tab3}],{$ln} "
                    . "{$tab2}]{$ln} {$tab}],{$ln} {$tab}// 数据表收藏{$ln} {$tab}'favorite' => [{$ln} {$tab2}"
                    . "'dbsample' => ['tbl_user', 'tbl_product', 'tbl_category'],{$ln} {$tab}],{$ln} {$tab}// 快速查"
                    . "询, 支持在 SQL 命令中用 {#object} 代替数据表名称{$ln} {$tab}'fastsqls' => [{$ln} {$tab2}// 用户表"
                    . "{$ln} {$tab2}'tbl_user' => [{$ln} {$tab3}'simple' => [{$ln} {$tab4}'text' => '简单查',{$ln} "
                    . "{$tab4}'remark' => '较少字段查询',{$ln} {$tab4}'sql' => 'select `id`, `group_id`, `username`, "
                    . "`nickname`, `level`, `gender`, `money`, `score`, '{$ln} {$tab5}. '`status` from {#object} "
                    . "order by id desc limit 50;'{$ln} {$tab3}]{$ln} {$tab2}],{$ln} {$tab}],{$ln} {$tab}// 登录账号"
                    . "配置{$ln} {$tab}'auth' => [{$ln} {$tab2}// 允许登录的帐号列表['登录名'=>'密码']{$ln} {$tab2}"
                    . "'admins' => ['myadmin' => 'passWd123', 'mydbm' => 'passWd123'],{$ln} {$tab2}// 允许访问的 ip 地"
                    . "址列表{$ln} {$tab2}'ips' => ['127.0.0.1', '110.52.27.168'],{$ln} {$tab2}// 指定页面访问验证类型："
                    . "0=无需验证;1=帐号密码验证;2=IP验证;3=混合验证(先 IP 验证，IP 验证失败则转入帐号密码验证){$ln} {$tab2}"
                    . "'current_authtype' => 1,{$ln} {$tab2}// ==================={$ln} {$tab2}// TOKEN 设置{$ln} "
                    . "{$tab2}// ==================={$ln} {$tab2}// TOKEN_EXP 超时时间 (1 hours, 1 minutes, 30 seconds)"
                    . "{$ln} {$tab2}'token_exp' => '1 hours',{$ln} {$tab2}// TOKEN_AUD 允许运行的服务器列表（适用于分布式"
                    . "环境，如果是负载均衡 + 高防IP环境，则填入高防IP即可）{$ln} {$tab2}// 多个之间以逗号隔开，中间不要有"
                    . "空格，例如：127.0.0.1,110.52.27.168{$ln} {$tab2}'token_auth' => '127.0.0.1,110.52.27.168',{$ln} "
                    . "{$tab2}// TOKEN 签名密钥{$ln} {$tab2}'token_key' => 'U6rxInZ7!',{$ln} {$tab2}// TOKEN 自动延期时"
                    . "长(秒)，每次页面跳转操作会重新生成一个延期过的 TOKEN，类似 session 的效果，只要一直操作就一直不会过期"
                    . "{$ln} {$tab2}'token_referesh_second' => 1800,{$ln} {$tab}],{$ln}];{$ln}";
                file_put_contents($configfile, $cfgcontent);
                echo '初始化成功！您需要修改配置文件信息才能正常使用！', PHP_EOL;
            }
        } catch (\Exception $ex) {
            echo $ex->getMessage();
        }
    }

    /**
     * 运行
     * @param int $argc 参数数量
     * @param array $argv 命令行参数数组
     * @param array $config 全局配置
     */
    public function run($argc, $argv, $config)
    {
        // 类自动加载功能
        mysal_splautoload();

        // __FILE_NAME__ cmdname params...
        if ($argc < 2) {
            echo '欢迎使用 ', APPNAME, ' ', VERSION, PHP_EOL, '查看使用帮助，请输入命令：php mysqladminlite.php --help', PHP_EOL;
            // 显示本地时间
            HtmlHelper::showLocaltime(date('Y 年 m 月 d 日 H 时 i 分 s 秒 星期')
                . '{{weekday}} 第 {{yearweek}} / {{yearweeks}} 周 第 {{yearday}} / {{yeardays}} 天' . PHP_EOL . '[{{motto}}]');
            exit();
        }
        // 命名名称
        $cmdname = $argv[1];
        // 解析参数数组
        $this->params = $this->getCliParams($argc, $argv);
        switch ($cmdname) {
            case '--help': // 帮助命令 --help --output y
                exit($this->execCmdHelp());
                break;
        }
        // 返回指定命令的帮助内容
        if ($this->hasParam('--help')) {
            exit($this->getCmdHelp($cmdname));
        }

        // 尝试读取配置文件
        $configfile = self::MYSAL_CONFIG_PATH . 'config.php';
        // 检测是否初始化
        if (!file_exists($configfile)) {
            $result = $this->stdin('程序尚未初始化，立即初始化？[y/n]');
            if ('y' == strtolower($result)) {
                $cmdname = 'init';
            } else {
                exit('程序尚未执行初始化，请先执行命令 init 进行初始化!');
            }
        }
        // 初始化命令 init，自动检查是否有缺失的目录并自动重新生成
        $this->execCmdInit($configfile, true);
        // 获取参数中的 --env 参数值
        $curenv = $this->getParam('--env');
        if (!$curenv && isset($config[ENVIRONMENTS][DEFAULT_DATABASE])) {
            $curenv =  $config[ENVIRONMENTS][DEFAULT_DATABASE];
        }
        // 判断是否需要连接数据库
        if ($curenv) {
            // 数据库配置解析
            $server = ConfigHelper::getCurrentEnv($config, $curenv, true);
            // 实例化数据库操作类
            try {
                $pdoadapter = new PdoConnector($server);
            } catch (\Exception $ex) {
                exit($ex->getMessage());
            }
        } else {
            $pdoadapter = false;
        }
        $needdbconnect = true; // 是否需要连接数据库
        if (in_array($cmdname, ['init'])) {
            $needdbconnect = false;
        }
        if ($needdbconnect && !$pdoadapter) {
            exit('数据库适配器初始化失败，请检查配置文件的设置！');
        }
        // 通用参数        
        $paramIsForce = 'y' == $this->getParam('--force');
        $paramDatabase = $this->getParam('--database');
        $params = $this->getParams();
        // 执行命令
        switch ($cmdname) {
            case 'init':
                // 初始化命令 第一次初始化过后，每次命令都会自动执行初始化，检测是否有缺失的文件并补充                
                $this->execCmdInit($configfile, true);
                if (is_dir(self::MYSAL_ROOT_DIR)) {
                    exit('您已经初始化过了！');
                }
                break;
            case 'status':
                // 查看数据库连接状态 status --env development
                try {
                    $result = $pdoadapter->find('select version() as ver');
                    echo '数据库连接成功！', PHP_EOL, '数据库环境：', $curenv, PHP_EOL;
                    echo '数据库名称：', $pdoadapter->getEnvInfo('database'), PHP_EOL, '数据库版本：', $result['ver'];
                } catch (\Exception $ex) {
                    echo $ex->getMessage();
                }
                break;
                // 启动网站服务器
            case 'serv':
                $host = $this->getParam('host', '127.0.0.1');
                $port = $this->getParam('port', '8866');
                $url = sprintf('%s:%d', $host, $port);
                echo '服务器已启动，请在浏览器访问：http://', $url, PHP_EOL;
                passthru('php -S ' . $url);
                break;
                // 路径转换
            case 'pathtrans':
                $path = $this->getParam('--path');
                $node = $this->getParam('--node');
                $test = $this->getParam('--test');
                $ds = $this->getParam('--ds', DS);
                if (!$path || !$node) {
                    exit('--path 或 --node 参数无效！');
                }
                $pos = strrpos($path, $ds . $node . $ds);
                if ($pos) {
                    $count = substr_count(substr($path, $pos + 1), $ds);
                    echo '路径转换结果：', PHP_EOL, '节点 ', $node, ' 在全路径：', $path, ' 的相对路径：', PHP_EOL;
                    $path =                        str_repeat('../', $count);
                    echo $path, PHP_EOL;
                    if ($test) {
                        try {
                            $filepath = $path . 'pathtrans-test.txt';
                            file_put_contents($filepath, 'success');
                            echo '创建测试文件成功！路径：', $filepath;
                        } catch (\Exception $e) {
                            echo '创建测试文件失败：', $e->getMessage(), PHP_EOL, '路径：', $filepath;
                        }
                    }
                } else {
                    exit('没有找到节点信息！');
                }
                break;
            case 'db:status':
                // 查询数据库服务器状态 db:[status|variables] --env development
                $result = $pdoadapter->query('show status;');
                foreach ($result as $row) {
                    echo implode(' = ', $row), PHP_EOL;
                }
                break;
            case 'db:variables':
                $result = $pdoadapter->getDbVariables();
                foreach ($result as $row) {
                    echo implode(' = ', $row), PHP_EOL;
                }
                break;
            case 'db:select':
                // 数据库查询，注意 SQL 命令要加引号 db:select --env development --sql "select * from tbl_example limit 10;"
                $sql = $this->getParam('--sql');
                if (!$sql) {
                    $sql = $this->stdin('请输入 SQL 命令：');
                }
                if (!$sql) {
                    exit(PHP_EOL . '操作已取消！');
                }
                try {
                    $result = $pdoadapter->query($sql);
                } catch (\PDOException $ex) {
                    exit($ex->getMessage());
                }
                if (!$result) {
                    exit('没有查询到任何数据！');
                }
                $i = 0;
                foreach ($result as $row) {
                    $i++;
                    echo PHP_EOL, '# [', $i, '] ---------------------------------------', PHP_EOL;
                    foreach ($row as $k => $v) {
                        echo $k, ' = ', $v, PHP_EOL;
                    }
                }
                break;
            case 'db:exec':
                // db:exec --env development --sql "update tbl_example set flag=1 where id=1;"
                $sql = $this->getParam('--sql');
                if (!$sql) {
                    $sql = $this->stdin('请输入 SQL 命令：');
                }
                if (!$sql) {
                    exit(PHP_EOL . '操作已取消！');
                }
                try {
                    $result = $pdoadapter->exec($sql);
                } catch (\PDOException $ex) {
                    exit($ex->getMessage());
                }
                echo '受影响行数：' . $result;
                break;
            case 'migrate:create':
                // 生成迁移文件 migrate:create --file ExampleTable --env development --table tbl_example
                $file = $this->getParam(['-f', '--file']);
                $table = $this->getParam(['-t', '--table']);
                // $version = $this->getParam(['-v', '--version']);

                // 如果没有设置文件名参数，则自动从数据表生成文件名
                if (!$file && $table) {
                    $file = str_replace(' ', '', ucwords(str_replace('_', ' ', $table)));
                }
                if (!$file) {
                    exit('--file 参数或 --table 参数至少输入一个！');
                }
                if (strpos($file, '.')) {
                    $classname = substr($file, 0, strpos($file, '.'));
                } else {
                    $classname = $file;
                    $file .= '.php'; // 如果文件名没有后缀则自动加文件名后缀
                }
                $filepath = self::MYSAL_MIGRATIONS_PATH . date('YmdHis') . '_' . $file;
                if (file_exists($filepath) && !$paramIsForce) {
                    exit('数据种子文件已存在，如需覆盖，请使用参数 --force y。' . PHP_EOL . '路径：' . $filepath . PHP_EOL);
                }
                $marginfw = $config[MIGRATION_FRAMEWORK];
                $pdoadapter->exportTableSchemaAsMigrateForCli($paramDatabase, $marginfw, $table, $filepath);
                echo PHP_EOL, 'success!';
                break;
            case 'source':
                // 运行 SQL 脚本文件 source --env development --file tbl_example.sql
                $file = $this->getParam(['-f', '--file']);
                if (!$file) {
                    exit('必须输入文件名');
                }
                if (!strpos($file, '.sql')) {
                    $file .= '.sql';
                }
                $file = self::MYSAL_SQLS_PATH . $file;
                if (!file_exists($file)) {
                    exit('文件不存在! 文件路径：' . $file);
                }
                $sqlstmt = file_get_contents($file); // 普通命令解析
                $sqlstmts = SqlHelper::parseSqlStatement(explode("\n", $sqlstmt)); // textarea回车符号是\r,和OS平台无关
                $pdoadapter->showQueryResultForCli($sqlstmts, $curenv, $paramDatabase);
                break;
            case 'source:all':
                // 批量导入本地 sql 文件 source:all [--env development] --dir dbexample
                $prefix = $this->getParam('--prefix');
                $dir = $this->getParam('--dir');
                $dir = $dir ? (self::MYSAL_SQLS_PATH . $dir) : self::MYSAL_SQLS_PATH;
                if (!is_dir($dir)) {
                    exit('目录不存在! 目录路径：' . $dir);
                }
                $files = FileHelper::scandir($dir, false, '.sql', $prefix);
                if (!$files) {
                    exit('操作取消，原因是：没有任何文件！');
                }
                $i = 1;
                foreach ($files as $file => $filepath) {
                    echo $i, '. ', $file, PHP_EOL;
                    $i++;
                }
                echo '即将导入以上 ', count($files), ' 个 SQL 文件, ';
                if ('yes' != $this->stdin('确定导入请输入 [yes]：')) {
                    exit('用户取消操作！');
                }
                foreach ($files as $k => $file) {
                    $sqlstmt = file_get_contents($file); // 普通命令解析
                    $sqlstmts = SqlHelper::parseSqlStatement(explode("\n", $sqlstmt)); // textarea回车符号是\r,和OS平台无关
                    $pdoadapter->showQueryResultForCli($sqlstmts, $curenv, $paramDatabase);
                }
                echo '批量导入成功！';
                break;
            case 'fnx:create':
                // 创建外部扩展功能函数模板文件 fnx:create --file vendor/fnsample.php
                $file = $this->getParam('--file');
                if (!$file) {
                    exit('必须输入 --file 参数');
                }
                if (!strpos($file, '.php')) {
                    $file .= '.php';
                }
                $filename = self::MYSAL_FUNCTIONS_PATH . $file;
                if (file_exists($filename) && !$paramIsForce) {
                    exit('外部扩展功能函数模板文件已存在，如需覆盖，请使用参数 --force y。' . PHP_EOL . '路径：' . $filename . PHP_EOL);
                }
                if (!is_dir(dirname($filename))) {
                    mkdir(dirname($filename), 0777, true);
                }
                $tpl = '<?php{#ln}/**{#ln} * 文件名：' . $file . '{#ln} * 功能描述：请修改run()方法里面的逻辑代码, 然后输入'
                    . '执行命令：fnx --file ' . $file . '{#ln} * {#ln} * @version ' . date('Y-m-d') . '{#ln} */{#ln}/**'
                    . '{#ln} * 默认的功能入口函数{#ln} * @param PdoConnector $pdoadapter PDO操作对象实例{#ln} * @param '
                    . 'array $params 命令行参数数组, 参数名格式为 --name{#ln} * 默认提供以下方法：selectDb($dbname); '
                    . 'getTables($dbname); getColumns($dbname,$tablename);{#ln} */{#ln}function run($pdoadapter, '
                    . '$params){#ln}{{#ln} {#tab}// 请将以下示例功能代码替换为您的逻辑代码{#ln} {#tab}// ...{#ln}{#ln} '
                    . '{#tab}// 数据库名称，默认为空则读取环境配置中的默认数据库 {#ln} {#tab}$dbname = \'\';'
                    . '{#ln} {#tab}// 读取环境配置中的当前数据库名称{#ln} {#tab}// $dbname=$pdoadapter->getEnvInfo'
                    . '(\'database\');{#ln} {#tab}// 如果指定与环境配置不同的数据库则执行切换数据库命令{#ln} {#tab}'
                    . 'if ($dbname) $pdoadapter->selectDb($dbname);{#ln} {#tab}// 获取数据库所有数据表的表信息，字段包括: '
                    . 'TABLE_NAME, TABLE_TYPE, ENGINE, DATA_LENGTH, CREATE_TIME, TABLE_COLLATION, TABLE_COMMENT{#ln} '
                    . '{#tab}$tableInfos = $pdoadapter->getTables($dbname);{#ln} {#tab}// 循环遍历数据表信息{#ln} '
                    . '{#tab}$content = \'\';{#ln}{#tab}foreach ($tableInfos as $tableinfo) {{#ln} {#tab2}$tablename = '
                    . '$tableinfo[\'TABLE_NAME\'];{#ln} {#tab2}// 获取数据表中所有字段的信息，字段包括： COLUMN_NAME, '
                    . 'IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, EXTRA, '
                    . 'COLUMN_DEFAULT, COLUMN_TYPE, COLUMN_KEY, COLUMN_COMMENT{#ln} {#tab2}$columns = $pdoadapter->'
                    . 'getColumns($dbname, $tablename);{#ln} {#tab2}if (!$columns) continue;{#ln} {#tab2}$content = '
                    . '\'### \' . $tablename . PHP_EOL . PHP_EOL;{#ln} {#tab2}$content .= \'| 字段 | 数据类型 | 可空 | '
                    . '默认值 | 索引键 | 备注 |\' . PHP_EOL;{#ln} {#tab2}$content .= \'| --- | --- | --- | --- | --- |\' '
                    . '. PHP_EOL;{#ln} {#tab2}foreach ($columns as $col) {{#ln} {#tab3}$content .= \'| \' . '
                    . '$col[\'COLUMN_NAME\'] . \' | \' . $col[\'COLUMN_TYPE\'] . \' | \' . $col[\'IS_NULLABLE\'] . \''
                    . ' | \';{#ln} {#tab3}$content .= $col[\'COLUMN_DEFAULT\'] . \' | \' . $col[\'COLUMN_KEY\'] . \' '
                    . '| \' . parse_dbcomment($col[\'COLUMN_COMMENT\']) . \' |\' . PHP_EOL;{#ln} {#tab2}}{#ln} {#tab2}'
                    . '$content .= PHP_EOL . PHP_EOL;{#ln} {#tab2}// 写入到文件 {#ln} {#tab2}$filename = \'mysal/data/exports/'
                    . 'fnxsample/\';{#ln} {#tab2}if (!is_dir($filename)) mkdir($filename, 0777, true);{#ln} {#tab2}'
                    . '$filename .= $tablename . \'.md\';{#ln} {#tab2}file_put_contents($filename, $content);{#ln} '
                    . '{#tab2}echo \'数据表 \', $tablename, \' 导出Markdown文件成功。 [文件路径：\', $filename, \']\', '
                    . 'PHP_EOL;{#ln}{#tab}}{#ln}{#tab}// 写入到文件{#ln} {#tab}$filename = \'mysal/data/exports/fnxsample/\';'
                    . '{#ln} {#tab}if (!is_dir($filename)) mkdir($filename, 0777, true);{#ln} {#tab}$filename .= '
                    . '\'summary.txt\';{#ln} {#tab}$content = \'一共读取了\' . count($tableInfos) . \'个数据表\';{#ln} '
                    . '{#tab}file_put_contents($filename, $content);{#ln} {#tab}echo \'导出成功！ [文件路径：\', '
                    . '$filename, \']\', PHP_EOL;{#ln}}{#ln}{#ln}/**{#ln} * 过滤数据库备注信息{#ln} * @param string '
                    . '$comment{#ln} * @return string{#ln} */{#ln}function parse_dbcomment($comment){#ln}{{#ln}{#tab}'
                    . 'return addslashes(str_replace([\'|\', PHP_EOL], [\'\|\', \' \'], $comment));{#ln}}{#ln}';
                file_put_contents($filename, str_replace(['{#ln}', '{#tab}', '{#tab2}', '{#tab3}'], [
                    PHP_EOL, "\t",
                    "\t\t", "\t\t\t"
                ], $tpl));
                echo '外部扩展功能函数模板文件创建成功！' . PHP_EOL . '路径：' . $filename;
                break;
            case 'fnx': // 运行外部扩展功能函数 fnx --file vendor/fnsample.php
                $file = $this->getParam('--file');
                $fn = $this->getParam('--function');
                if (!$file) {
                    exit('必须输入 --file 参数');
                }
                if (!strpos($file, '.php')) {
                    $file .= '.php';
                }
                $file = self::MYSAL_FUNCTIONS_PATH . $file;
                if (!file_exists($file)) {
                    exit('文件不存在！[' . $file . ']');
                }
                if (!$fn) {
                    $fn = 'run';
                }
                try {
                    require $file;
                    call_user_func($fn, $pdoadapter, $this->getParams(['--file']));
                    echo '命令调用成功!', PHP_EOL, '命令路径：', $file, '::', $fn, '()';
                } catch (\Exception $ex) {
                    echo $ex->getMessage();
                }
                break;
            case 'seed:create':
                // 创建测试数据文件 seed:create --env development --file ExampleTableSeeder --table tbl_example
                $table = $this->getParam('--table');
                $file = $this->getParam('--file');
                if (!$table) {
                    echo '必需输入 --table 参数';
                    exit;
                }
                if (!$pdoadapter->hasTable($table)) {
                    exit('数据表 ' . $table . ' 不存在!');
                }
                if (!$file) {
                    $file = mysal_table2classname($table);
                }
                if (!strpos($file, 'Seeder')) {
                    $file .= 'Seeder';
                }
                if (!strpos($file, '.php')) {
                    $file .= '.php';
                }
                $filepath = self::MYSAL_SEEDS_PATH . $file;
                if (file_exists($filepath) && !$paramIsForce) {
                    exit('数据种子文件已存在，如需覆盖，请使用参数 --force y。' . PHP_EOL . '路径：' . $filepath . PHP_EOL);
                }
                $pdoadapter->exportTableSchemaAsSeedForCli('', $table, $config[MIGRATION_FRAMEWORK], $filepath, true);
                echo '数据种子文件创建成功! ', PHP_EOL, '路径：', $filepath;
                break;
            case 'seed:autocreate':
                // seed:autocreate [--env development] [--prefix tbl_] [--force y] [--eltcount 10] [--dir dbsample]
                $table = $this->getParam('--table');
                $prefix = $this->getParam('--prefix');
                $eltcount = intval($this->getParam('--eltcount'));
                $pdir = $this->getParam('--dir');
                $dir = $pdir ? (self::MYSAL_SEEDS_PATH . $pdir . '/') : self::MYSAL_SEEDS_PATH;
                if (!is_dir($dir)) {
                    mkdir($dir);
                }
                $migrationfw = $config[MIGRATION_FRAMEWORK];
                $result = $pdoadapter->exportDbSchemaAsSeed($paramDatabase, $migrationfw, $dir, $paramIsForce, $eltcount, $prefix);
                echo '数据种子文件创建成功! ', PHP_EOL, '文件数量：', $result[0], '，实际创建文件数量：', $result[1];
                echo '，跳过文件数量：', ($result[2]), PHP_EOL, '路径：', $dir;
                if ($pdir) {
                    echo PHP_EOL, '您指定了生成子目录 ', $pdir, ' ，运行时需要把种子文件移动到 [seeds] 根目录才能正常使用！';
                }
                break;
            case 'seed:run':
                // 运行测试数据生成 seed:run [--env development] --file ExampleTableSeeder.php
                $table = $this->getParam('--table');
                $file = $this->getParam('--file');
                if (!$file) {
                    if ($table) {
                        $file = mysal_table2classname($table) . 'Seeder.php';
                    } else {
                        exit('必须输入文件名');
                    }
                }
                if (!strpos($file, '.php')) {
                    $file .= '.php';
                }
                if (false !== strpos($file, '/')) {
                    exit('种子文件必须移动到 [seeds] 根目录下！');
                }

                if (!file_exists(self::MYSAL_SEEDS_PATH . $file)) {
                    exit('种子文件不存在! 路径：' . self::MYSAL_SEEDS_PATH . $file);
                }
                if (strpos($file, '.')) {
                    $file = substr($file, 0, strpos($file, '.'));
                }
                $classname =str_replace('/','\\',ltrim(self::MYSAL_SEEDS_PATH,'./')) . $file;
                try {
                    $class = new $classname();
                    $result = $class->run(new Table($pdoadapter));
                    if (!$result['status']) {
                        foreach ($result['data'] as $message) {
                            echo $message, PHP_EOL;
                        }
                    } else {
                        echo '测试数据写入成功！', PHP_EOL, '受影响行数：', $result['affected_rows'];
                    }
                } catch (\Exception $ex) {
                    exit('出错啦：' . $ex->getMessage());
                }
                break;
            case 'seed:autorun':
                // 自动运行测试数据生成 seed:autorun [--env development] [--database dbsample2] [--prefix tbl_] [--eltcount 0]
                $prefix = $this->getParam('--prefix');
                $eltcount = $this->getParam('--eltcount', 0);
                $tables = $pdoadapter->getEmptyTables($paramDatabase, $eltcount, $prefix);
                $classes = [];
                foreach ($tables as $table => $data) {
                    $classname = mysal_table2classname($table);
                    $filename = self::MYSAL_SEEDS_PATH . $classname . 'Seeder.php';
                    if (!file_exists($filename)) {
                        continue;
                    }
                    $classes[$table] =str_replace('/','\\',ltrim(self::MYSAL_SEEDS_PATH,'./')) . $classname . 'Seeder';
                }
                if (!$classes) {
                    echo '操作取消，原因是：1.没有空的数据表需要写入测试数据; 2.没有数据表对应的数据种子文件!', PHP_EOL;
                    echo '记录为空的数据表数量：', count($tables);
                    exit();
                }
                try {
                    foreach ($classes as $table => $classname) {
                        $class = new $classname();
                        $result = $class->run(new Table($pdoadapter));
                        if (!$result['status']) {
                            foreach ($result['data'] as $message) {
                                echo $message, PHP_EOL;
                            }
                        } else {
                            echo '数据表 ', $table, ' 写入测试数据成功！ 受影响行数：', $result['affected_rows'], PHP_EOL;
                        }
                    }
                } catch (\Exception $ex) {
                    exit('出错啦：' . $ex->getMessage());
                }
                break;
            case 'export:database':
                // 导出整个数据库结构和表数据
                $prefix = $this->getParam('--prefix'); // 数据表前缀
                if (!$paramDatabase) {
                    $paramDatabase = $pdoadapter->getEnvInfo('database');
                }
                $file = self::MYSAL_EXPORTS_PATH . $paramDatabase;
                // 导出数据库架构
                $pdoadapter->exportDbSchemaAsSqlForCli($paramDatabase, '', false, $file . '.sql', $prefix);
                // 导出所有表的数据
                $tables = $pdoadapter->getTables($paramDatabase);
                foreach ($tables as $row) {
                    $table = $row['TABLE_NAME'];
                    if ($prefix && 0 !== strpos($table, $prefix)) {
                        continue;
                    }
                    $sql = "select * from `$table`";
                    $pdoadapter->exportQueryDataAsSqlForCli($paramDatabase, $table, $sql, $file . '_' . $table . '.sql');
                }
                echo '数据导出成功！一共 ', count($tables), ' 个数据表 [', $file, ']';
                break;
            case 'export:data':
            case 'mock':
                // 导出数据表数据 export:data --table tbl_example [--sql "select * from tbl_example"]
                $table = $this->getParam('--table'); // 数据表名称,没有指定名称则导出所有表
                $sql = $this->getParam('--sql');
                $filetype = $this->getParam('--filetype'); // 文件类型，excel,html,markdown,sql,json
                if ($cmdname == 'mock') {
                    $filetype = 'json';
                }
                if (!$table && !$sql) {
                    exit('必需输入 --table 参数和 --sql 参数的其中一个！');
                }
                if (!$sql) {
                    $sql = "select * from `$table`";
                }
                $file = self::MYSAL_EXPORTS_PATH . ($table ? $table : ('query-' . date('YmdHis')));
                switch ($filetype) {
                    case 'excel':
                        $file .= '.xls';
                        $pdoadapter->exportQueryDataAsExcelForCli($paramDatabase, $table, $sql, $file);
                        break;
                    case 'html':
                        $file .= '.html';
                        $pdoadapter->exportQueryDataAsHtmlForCli($paramDatabase, $table, $sql, $file);
                        break;
                    case 'json':
                        $file .= '.json';
                        $pdoadapter->exportQueryDataAsJsonForCli($paramDatabase, $table, $sql, $file);
                        break;
                    case 'markdown':
                        $file .= '.md';
                        $pdoadapter->exportQueryDataAsMarkdownForCli($paramDatabase, $table, $sql, $file);
                        break;
                    default:
                        $file .= '.sql';
                        $pdoadapter->exportQueryDataAsSqlForCli($paramDatabase, $table, $sql, $file);
                        break;
                }
                echo '数据导出成功! [', $file, ']';
                break;
            case 'export:schema':
                // 导出数据库结构 export:schema [--database dbsample2] [--table tbl_example]
                // [--filetype excel|sql|html] [--eachfile y] [--prefix tbl_]
                $table = $this->getParam('--table'); // 数据表名称，没有指定名称则导出所有表
                $filetype = $this->getParam('--filetype'); // 文件类型，excel,html,sql
                $prefix = $this->getParam('--prefix'); // 数据表前缀
                if (!$paramDatabase) {
                    $paramDatabase = $pdoadapter->getEnvInfo('database');
                }
                $file = self::MYSAL_EXPORTS_PATH . ($table ? $table : $paramDatabase);
                switch ($filetype) {
                    case 'excel':
                        $file .= '.xls';
                        if ($table) {
                            $pdoadapter->exportTableSchemaAsExcelForCli($paramDatabase, $table, $file);
                        } else {
                            $pdoadapter->exportDbSchemaAsExcelForCli($paramDatabase, $file, $prefix);
                        }
                        break;
                    case 'html':
                        $file .= '.html';
                        if ($table) {
                            $pdoadapter->exportTableSchemaAsHtmlForCli($paramDatabase, $table, $file);
                        } else {
                            $pdoadapter->exportDbSchemaAsHtmlForCli($paramDatabase, $file, $prefix);
                        }
                        break;
                    default:
                        $file = self::MYSAL_EXPORTS_PATH;
                        $iseachfile = 'y' == $this->getParam('--eachfile');
                        if ($iseachfile) {
                            $file .= $paramDatabase . '/';
                            if (!is_dir($file)) {
                                mkdir($file);
                            }
                            $pdoadapter->exportDbSchemaAsSqlForCli($paramDatabase, $table, true, $file, $prefix);
                        } else {
                            $file .= ($table ? $table : $paramDatabase) . '.sql';
                            $pdoadapter->exportDbSchemaAsSqlForCli($paramDatabase, $table, false, $file, $prefix);
                        }
                        break;
                }
                echo '数据表结构导出成功! [', $file, ']';
                break;
            case 'template:sql':
                // 创建数据库命令模板文件
                // template:sql --operate create_table|alter_table|create_view|create_routine --file tbl_test.sql
                $operate = $this->getParam('--operate');
                $file = $this->getParam('--file');
                if (!$file) {
                    exit('必须填写 --file 参数！');
                }
                if (!strpos($file, '.sql')) {
                    $file .= '.sql';
                }
                $filename = self::MYSAL_SQLS_PATH . $file;
                FileHelper::mkdir($filename);
                file_put_contents($filename, SqlHelper::getDefaultExampleSql('eg' . $operate, false));
                echo '创建SQL模板文件成功! 路径：', $filename;
                break;
            case 'stub:crud':
                // 生成数据库增删改查对应的存根模板文件
                // stub:crud --table fa_test --project fastlara/admin --force y --init y
                $project = $this->getParam('--project');
                if (!$project) {
                    exit('必须填写 -- project 参数！');
                }
                $stubProjectRootPath = ltrim(self::MYSAL_EXTENSIONS_STUBS_PATH,'./') . $project . '/';
                $stubsHandlerFile = $stubProjectRootPath . 'Crud.php';
                $classname = str_replace('/', '\\', substr($stubsHandlerFile, 0, -4));
                if (!file_exists($stubsHandlerFile)) {
                    exit('Handler 文件 ' . $stubsHandlerFile . ' 不存在！请运行命令 stub:new --name yourHandler '
                        . '新建 Handler 文件。');
                }
                $stubsCfgFile = $stubProjectRootPath . 'config.php';
                if (!file_exists($stubsCfgFile)) {
                    exit($stubsCfgFile . ' 配置文件不存在！请重新运行命令 stub:init 进行初始化。');
                }
                $stubsConfig = require $stubsCfgFile;
                if (!$stubsConfig || !is_array($stubsConfig)) {
                    exit("项目 $project 配置内容无效! 路径：$stubsCfgFile");
                }
                if ($cmdname == 'stub:remove') {
                    $params['--remove'] = 'y';
                }
                // 调用处理程序文件
                try {
                    $class = new $classname();
                    $class->run($pdoadapter, $stubsConfig, $params, $stubProjectRootPath);
                } catch (\Exception $ex) {
                    exit('出错啦：' . $ex->getMessage());
                }
                break;
            case 'stub:make': // 生成存根模板文件 stub:make --name controller:SampleController --project powersdk/sample --force y
            case 'stub:remove':
                $project = $this->getParam('--project');
                if (!$project) {
                    exit('必须填写 -- project 参数！');
                }
                $stubProjectRootPath =ltrim(self::MYSAL_EXTENSIONS_STUBS_PATH,'./') . $project . '/';
                $handlerFile = $stubProjectRootPath . 'Make.php';
                $classname = str_replace('/', '\\', substr($handlerFile, 0, -4));
                if (!file_exists($handlerFile)) {
                    exit('Handler 文件 ' . $handlerFile . ' 不存在！请运行命令 stub:new --name yourHandler 新建 '
                        . 'Handler 文件。');
                }
                $stubsCfgFile = $stubProjectRootPath . 'config.php';
                if (!file_exists($stubsCfgFile)) {
                    exit($stubsCfgFile . ' 配置文件不存在！请重新运行命令 stub:init 进行初始化。');
                }
                $stubsConfig = require $stubsCfgFile;
                if ($cmdname == 'stub:remove') {
                    $params['--remove'] = 'y';
                }
                // 调用处理程序文件
                try {
                    $class = new $classname();
                    $class->run($pdoadapter, $stubsConfig, $params, $stubProjectRootPath);
                } catch (\Exception $ex) {
                    exit('出错啦：' . $ex->getMessage());
                }
                break;
            default:
                echo '不支持该命令!';
                break;
        }
    }
}

/**
 * 数据表操作类
 * 用于数据迁移功能
 * @version 2020-2-14
 */
class Table
{
    /**
     * PdoAdapter实例
     * @var PdoAdapter
     */
    protected $pdo = null;
    /**
     * 数据表名称
     * @var string
     */
    protected $tablename = '';
    /**
     * 数据表属性数组
     * @var array
     */
    protected $tableAttr = [];
    /**
     * 数据表字段数组
     * @var array
     */
    protected $fields = [];
    /**
     * 构造方法
     * @param PdoAdapter $pdoadapter
     */
    public function __construct($pdoadapter)
    {
        $this->pdo = $pdoadapter;
    }
    /**
     * 设置数据表名称和属性
     * @param string $name 数据表名称
     * @param array $option 数据表属性,支持三个属性：comment, charset, engine
     * @return Table 返回实例自身
     */
    public function name($name, $option = null)
    {
        $this->tableName = $name;
        if ($option) {
            $this->tableAttr = $option;
        }
        return $this;
    }
    /**
     * 添加字段设置
     * @param string $name 字段名称
     * @param string $type 字段类型
     * @param array $option 字段属性
     */
    public function addColumn($name, $type, array $option = [])
    {
        $this->fields[] = ['name' => $name, 'type' => $type, 'option' => $option];
        return $this;
    }
    /**
     * 添加索引
     */
    public function addKey($name, $field)
    {
    }
    /**
     * [不可用]创建表
     * 未完成，暂不可用，等有闲余时间再完善
     */
    public function create($dropIfExists = false)
    {
        $sql = 'drop table if exists ``';
        $sql = 'create table `` (';
        if (!empty($fields = $this->fields)) {
            foreach ($fields as $col) {
                $sql .= $col['name'];
                $sql .= $col['type'];
                if (false === $col['null']) {
                }
            }
        }
        $tableattrs = ['engine' => 'engine', 'charset' => 'default charset', 'comment' => 'comment'];
        if (!empty($tableAttr = $this->tableAttr)) {
            foreach ($tableattrs as $k => $v) {
                if (!empty($tableAttr[$k])) {
                    $sql .= ' ' . $v . '=' . $tableAttr[$k];
                }
            }
            $sql .= ';';
        }
    }
    /**
     * 更新表数据
     * @param array $data 数据数组
     * @param string $where 更新条件
     * @param string $table 临时指定的数据表名
     */
    public function update($data, $where, $table = '')
    {
        if (!$table) {
            $table = $this->tableName;
        }
        $sql = 'update ' . $table . ' set ';
        foreach ($data as $k => $v) {
            $sql .= "`$k` = '$v'";
        }
        if ($where) {
            $sql .= ' where ' . $where;
        }
        return $this->pdo->execute($sql);
    }
    /**
     * 写入数据
     * @param array $data 数据数组
     * @param string $table 临时指定的数据表名
     */
    public function insert($table, $data)
    {
        if (!$data) {
            return false;
        }
        if (!$table) {
            $table = $this->tableName;
        }
        try {
            if (is_array(current($data))) {
                $sqls = [];
                foreach ($data as $row) {
                    $sql = 'insert into ' . $table . ' (`' . implode('`,`', array_keys($row)) . '`) values ';
                    $sql .= '(\'' . implode('\',\'', array_values($row)) . '\');';
                    $sqls[] = $sql;
                }
                return $this->pdo->batchQuery($sqls, true);
            } else {
                $sql = 'insert into ' . $table . ' (`' . implode('`,`', array_keys($data)) . '`) values ';
                $sql .= '(\'' . implode('\',\'', array_values($data)) . '\');';
                return $this->pdo->execute($sql);
            }
        } catch (\PDOException $ex) {
            throw $ex;
        }
    }
    /**
     * 清空数据表
     */
    public function truncate($table)
    {
        return $this->pdo->exec('truncate table ' . $table);
    }
}

/**
 * 用户登录认证类
 * @version 2017-12-15
 */
class JWTLite
{

    /**
     * TOKEN_EXP 超时时间 (1 hours, 1 minutes, 30 seconds)
     * @var string
     */
    public static $TOKEN_EXP = '1 hours';

    /**
     * TOKEN_AUD 允许运行的服务器列表
     * 适用于分布式环境，如果是负载均衡 + 高防 IP 环境，则填入高防 IP 即可，多个之间以逗号隔开，中间不要有空格
     * @var string
     */
    public static $TOKEN_AUD = '127.0.0.1,47.95.44.177,47.94.100.12,47.93.45.33,47.93.42.85';

    /**
     * TOKEN 签名密钥
     * @var string
     */
    public static $TOKEN_KEY = 'InZ7!';

    /**
     * TOKEN 自动延期时长, 每次页面跳转操作会重新生成一个延期过的 TOKEN，类似 session 的效果，只要一直操作就一直不会过期
     * @var integer
     */
    public static $TOKEN_REFRESH_SECOND = 1800;

    /**
     * 页面访问验证配置
     * @var array
     */
    public static $AUTH_CONFIG = [];

    /**
     * 登录结果
     * @var array compact('loginid', 'token', 'loginexp')
     */
    public static $LOGIN_RESULT = [];

    /**
     * 初始化
     * @param array $config 全局配置
     */
    public static function init($config)
    {
        if (empty($config['auth'])) {
            exit('请配置 AUTH 节点信息!');
        }
        self::$AUTH_CONFIG = $config['auth'];
        // TOKEN_EXP 超时时间 (1 hours, 1 minutes, 30 seconds)
        self::$TOKEN_EXP = mysal_array_value($config, 'auth.token_exp', '1 hours');
        // TOKEN_AUD 允许运行的服务器列表（适用于分布式环境，如果是负载均衡 + 高防 IP 环境，则填入高防 IP 即可）
        // 多个之间以逗号隔开，中间不要有空格，例如：127.0.0.1,110.52.27.168
        self::$TOKEN_AUD = mysal_array_value($config, 'auth.token_auth', '127.0.0.1,110.52.27.168');
        // TOKEN 签名密钥
        self::$TOKEN_KEY = mysal_array_value($config, 'auth.token_key', 'U6rxInZ7!');
        // TOKEN 自动延期时长，每次页面跳转操作会重新生成一个延期过的 TOKEN，类似 session 的效果，只要一直操作就一直不会过期
        self::$TOKEN_REFRESH_SECOND = mysal_array_value($config, 'auth.token_referesh_second', 1800);
    }

    /**
     * 生成签名
     * @param array $data
     * @return array array(exp,sign,...)
     * @version 2017-11-3
     */
    public static function genSign(array $data = [])
    {
        $tokenInfo = array(
            "exp" => strtotime("30 minutes"), // 默认 TOKEN 有效期 30 分钟
            "aud" => self::$TOKEN_AUD,
        );
        $data = array_merge($tokenInfo, $data);
        ksort($data);
        $md5Str = md5(self::$TOKEN_KEY . json_encode($data));
        $data['sign'] = substr($md5Str, 20, 6);
        unset($data['aud']);
        return $data;
    }

    /**
     * 验证签名
     * @param array $data array(exp,sign,...)
     * @return array
     * @version 2017-11-3
     */
    public static function checkSign($data)
    {
        if (empty($data['exp']) || empty($data['sign'])) {
            return array('status' => false, 'info' => '签名内容不规范');
        }
        $exp = $data['exp'];
        if ($exp < time()) {
            return array('status' => false, 'info' => '签名已过期');
        }
        if (self::$TOKEN_AUD) {
            $audArr = explode(',', self::$TOKEN_AUD);
            $hostIp = self::getHostIP();
            if (!in_array($hostIp, $audArr)) {
                return array(
                    'status' => false,
                    'info' => '服务器不在白名单<span style="display:none">' . $hostIp . '</span>'
                );
            }
        }
        $postSign = $data['sign'];
        unset($data['sign']);
        $data['aud'] = self::$TOKEN_AUD;
        ksort($data); // aud,exp,user
        $localSign = substr(md5(self::$TOKEN_KEY . json_encode($data)), 20, 6);
        if ($localSign != $postSign) {
            return array('status' => false, 'info' => '签名非法');
        }
        return array('status' => true, 'info' => '');
    }

    /**
     * 获取表单令牌
     * @return string
     * @version 2017-11-3 由 session 改为 token
     */
    public static function getToken($data)
    {
        return base64_encode(json_encode(self::genSign($data)));
    }

    /**
     * 解析 token
     * @param string $post_token
     * @return array
     * @version 2017-11-3 added.
     */
    public static function parseToken($post_token = '')
    {
        if (!$post_token) {
            $post_token = htmlspecialchars(trim(HtmlHelper::input('token')));
        }
        if (!$post_token) {
            return ['status' => false, 'info' => 'TOKEN 为空'];
        }
        $json = base64_decode($post_token);
        if (false === $json) {
            return ['status' => false, 'info' => 'TOKEN 内容无效'];
        }
        $post = json_decode($json, true);
        if (!$post) {
            return ['status' => false, 'info' => 'TOKEN 格式错误'];
        }
        $sigResult = self::checkSign($post);
        if (!$sigResult['status']) {
            return $sigResult;
        }
        return ['status' => true, 'info' => $post];
    }

    /**
     * 生成新的延期过的 token
     * @param integer $exp 延期时间
     * @param string $token 旧的 token 字符串
     * @return string
     * @version 2017-11-29
     */
    public static function refreshToken($exp, $token)
    {
        $tokenInfo = self::parseToken($token);
        $data = $tokenInfo['info'];
        unset($data['sign']);
        $data['exp'] = strtotime(self::$TOKEN_EXP) + self::$TOKEN_REFRESH_SECOND;
        return self::getToken($data);
    }

    /**
     * 用户登录操作
     * @param array $admins
     * @param array $urlparams 额外的浏览器参数
     */
    public static function doLogin($admins, $urlparams = [])
    {
        $posttoken = htmlspecialchars(HtmlHelper::input('login_token'));
        if ($posttoken) {
            $tokenInfo = self::parseToken($posttoken);
            $goback_btn = ' <a href="?login" style="text-decoration:none;padding:2px 5px;border:solid 1px #2b6cb0;
                    background-color:#4299e1;color:#FFF;border-radius:.25rem;border-bottom-width:4px;">返回</a>';
            if (!$tokenInfo['status']) {
                exit($tokenInfo['info'] . $goback_btn);
            }
            $loginid = strval(htmlspecialchars(HtmlHelper::input('user')));
            $loginpwd = htmlspecialchars(HtmlHelper::input('pass'));
            if (!$loginid || !$loginpwd) {
                exit('表单填写不完整!' . $goback_btn);
            }
            if (!array_key_exists($loginid, $admins)) {
                exit('帐号不存在!' . $goback_btn);
            }
            if ($loginpwd != $admins[$loginid]) {
                exit('密码错误!' . $goback_btn);
            }
            $url = '?token=' . self::getToken(array('exp' => strtotime(self::$TOKEN_EXP), 'user' => $loginid));
            if ($urlparams) {
                $url .= '&' . http_build_query($urlparams);
            }
            header('location:' . $url);
        } else {
            echo '<html><head><title></title><meta charset="UTF-8" />', HtmlHelper::getResourceLink('bootstrap', 'css'),
                '<style>body{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;',
                'padding-top:40px;padding-bottom:40px;background-color:#f5f5f5;}.form-signin{width:100%;padding:15px;',
                'margin:auto;}@media (min-width:991.98px){.form-signin{max-width:330px;}}</style></head>',
                '<body class="text-center"><form method="post" action="?action=login" class="form-signin">',
                '<h2>Welcome!</h2><div class="form-group"><input class="form-control" type="text" name="user" ',
                'placeholder="请输入用户名 / LoginID" required="required" /> <input class="form-control" ',
                'type="password" name="pass" placeholder="请输入密码 / Password" required="required" /></div>',
                '<button class="btn btn-primary btn-block" type="submit" title="Login">立即登录</button>',
                '<button class="btn btn-secondary btn-block" type="reset" title="Reset">重置</button>',
                '<input type="hidden" name="login_token" value="',
                self::getToken(array('exp' => strtotime('3 minutes'), 'action' => 'login')),
                '" />';
            if ($urlparams) {
                foreach ($urlparams as $key => $val) {
                    echo '<input type="hidden" name="', $key, '" value="', $val, '" />';
                }
            }
            echo '<p class="mt-3 text-muted">', $_SERVER['REMOTE_ADDR'], '</p>';
            echo '</form></body></html>';
        }
    }

    /**
     * 访问权限验证
     * @param array $urlparams
     */
    public static function checkLogin($urlparams = [])
    {
        $curAuthtypeId = self::$AUTH_CONFIG['current_authtype'];
        $curAuthStatus = true; // 权限验证状态
        if ($curAuthtypeId == 2 || $curAuthtypeId == 3) { // IP 验证
            $http_host = $_SERVER['REMOTE_ADDR'];
            if (!in_array($http_host, self::$AUTH_CONFIG['ips'])) {
                $curAuthStatus = false; // IP 验证失败
                if ($curAuthtypeId == 2) {
                    exit('IP [' . $http_host . '] 禁止访问!');
                }
            }
        }

        $loginid = false; // 用户登录 ID
        $loginexp = ''; // token 过期时间提示
        $token = htmlspecialchars(trim(HtmlHelper::input('token'))); // 用户登录会话 Token
        if ($curAuthtypeId == 1 || ($curAuthtypeId == 3 && !$curAuthStatus)) { // 帐号密码验证
            if ($token) { // 解析登录会话 token 信息
                $tokenInfo = self::parseToken($token);
                if ($tokenInfo['status']) {
                    $loginid = $tokenInfo['info']['user'];
                    $loginexp = date('Y-m-d H:i:s', $tokenInfo['info']['exp']);
                    // 自动续期功能 2017-11-29
                    if (self::$TOKEN_REFRESH_SECOND > 0) {
                        $refreshToken = self::refreshToken(self::$TOKEN_REFRESH_SECOND, $token);
                        $token = $refreshToken;
                    }
                } else {
                    header('location:?msg=' . $tokenInfo['info']);
                    exit();
                }
            }
            // 注销登录
            if (HtmlHelper::input('action') == 'logout') {
                header('location:?msg=logout-success');
                exit;
            }
            if (!$loginid) {
                self::doLogin(self::$AUTH_CONFIG['admins'], $urlparams);
                exit;
            }
        }
        self::$LOGIN_RESULT = compact('loginid', 'token', 'loginexp');
    }

    /**
     * 返回服务器 IP
     * 参考：SERVER_NAME 和 HTTP_HOST 的区别 (http://blog.sina.com.cn/s/blog_6d96d3160100q39x.html)
     * @version 1.0 2017-10-13 Added.
     */
    public static function getHostIP()
    {
        //return $_SERVER['HTTP_HOST']; // 有服务器域名则优先返回域名，否则返回服务器 ip
        return gethostbyname($_SERVER['SERVER_NAME']); // 返回服务器IP
    }
}

/**
 * SQL 命令助手类
 */
class SqlHelper
{
    /**
     * 过滤数据库备注信息
     * @param string $comment
     * @return string
     */
    public static function parseDbComment($comment)
    {
        return addslashes(str_replace(['|', PHP_EOL], ['\|', ' '], $comment));
    }

    /**
     * 数据库类型转换为脚本类型
     * @param string $adapterType
     * @return string
     */
    public static function parseDbTypeToPhinxType($adapterType)
    {
        $types = [
            'char' => ['type' => 'string', 'is_number' => false], // char,varchar
            'bigint' => ['type' => 'biginteger', 'is_number' => true], // 注意顺序要在 int 前面
            'smallint' => ['type' => 'smallint', 'is_number' => true],
            'int' => ['type' => 'integer', 'is_number' => true], // tinyint,smallint,mediumint
            'float' => ['type' => 'float', 'is_number' => true],
            'double' => ['type' => 'decimal', 'is_number' => true],
            'decimal' => ['type' => 'decimal', 'is_number' => true],
            'year' => ['type' => 'year', 'is_number' => true],
        ];
        foreach ($types as $key => $value) {
            if (false !== stripos($adapterType, $key)) {
                return $value;
            }
        }
        // enum 或 set 类型
        if (strpos($adapterType, "'")) {
            $pos = strpos($adapterType, '(');
            return [
                'type' => substr($adapterType, 0, $pos), 'is_number' => false,
                'values' => substr($adapterType, $pos + 1, -1)
            ];
        }
        return ['type' => $adapterType, 'is_number' => false];
    }
    /**
     * 加载 sql 文件为分号分割的数组，注意最后一条命令语句要以分号结尾，否则最后一条命令不会解析出来
     * 支持存储过程和函数提取，自动过滤注释
     * @param string $path 文件路径
     * @return boolean|array
     * @version 1.0 2015-5-27 Added.
     * @version 1.1 2016-9-6 修正 drop function 或 drop procedure 无法解析的问题
     */
    public static function parseSqlStatement($lines, $routinesplitor = ';;')
    {
        // $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); // 读取文件到数组
        $outlines = [];
        $stmt = '';
        $isroutine = false;
        $routinesplitorend = $routinesplitor[0];
        $isskip = false; // 是否过滤批量注释
        foreach ($lines as $line) {
            $line = trim($line); // 过滤头尾空格
            if (!$line) {
                continue; // 过滤空行
            }
            if (0 === strpos($line, '/*')) { // 批量注释开始
                if (false === strpos($line, '*/')) { // 如果不是单行注释，则从此行开始，一直过滤到发现*/字符为止
                    $isskip = true; // 过滤批量注释
                }
                continue; // 该批量注释是单行注释
            }
            if ($isskip && (false !== strpos($line, '*/'))) { // 批量注释结束
                $isskip = false;
                continue;
            }
            if ($isskip) {
                continue; // 过滤批量注释
            }
            if (0 === strpos($line, '-- ')) {
                continue; // 过滤单行注释
            }
            $lowerline = strtolower($line);
            // 过滤注释 ...
            // 提取存储过程和函数
            if (!$isroutine && 0 === strpos($lowerline, 'delimiter ' . $routinesplitor)) {
                $isroutine = true;
                continue;
            }
            if ($isroutine) {
                // drop 语句独立一行
                if (0 === strpos($lowerline, 'drop function ') || 0 === strpos($lowerline, 'drop procedure ')) {
                    $outlines[] = $line;
                    $stmt = '';
                    continue;
                }
                if (0 === strpos($lowerline, 'delimiter ' . $routinesplitorend)) {
                    $isroutine = false;
                    $outlines[] = $stmt;
                    $stmt = '';
                    continue;
                }
                $stmt .= $line . ' ';
                continue;
            }
            // 提取普通语句，支持一个语句拆分成很多行的写法，注意语句结束后必须以分号结尾
            $stmt .= $line . ' '; // 串联行内容要加个空格，防止 select a\rfrom table，拼凑成 select afrom table
            if (false !== strpos($line, ';') && strlen($line) == (strpos($line, ';') + 1)) {
                $outlines[] = $stmt;
                $stmt = '';
            }
        }
        // 兼容单sql语句，且没有以分号结尾的情况
        // if (!$outlines && $stmt) { // 这样写最后一条命令如果没有分号结尾会解析不出来
        if ($stmt) {
            $outlines[] = $stmt;
        }
        return $outlines;
    }
    /**
     * 加载 sql 文件为分号分割的数组
     * parseSqlStatement() 的替代方法，去除单行注释内容有问题，还没有经过测试，暂时不要用
     * 思路是：提取引号内容--将引号内容中的注释和分号转义保留--分割分号数组--循环数组并对数组元素还原注释转义
     * @version 2020-3-2
     */
    private function parseBatchSqls($sqltext)
    {
        $symbols = ['\\\'' => '#@sq#', '\\\"' => '#@dq#'];
        $sqltext = str_replace(array_keys($symbols), array_values($symbols), $sqltext);
        $pattern = '/([\'"])([^\'"\.]*?)\1/';
        $pattern = '/[\'"]([^\'"]*)[\'"]/';
        $sqltext = preg_replace_callback($pattern, function ($a) {
            $a[0] = str_replace(';', '#@sem#', $a[0]); // 分号处理
            $a[0] = preg_replace('/(--.*)|(\/\*(.|\s)*?\*\/)|(\n)/', '', $a[0]); // 注释处理
            return $a[0];
        }, $sqltext);
        $sqls = explode(';', $sqltext); // 拆分成数组
        $sqls = array_filter($sqls); // 过滤空元素
        $outsqls = [];
        $symbols[';'] = '#@sem#';
        foreach ($sqls as $sql) {
            $outsqls[] = str_replace(array_values($symbols), array_keys($symbols), $sql);
        }
        return $outsqls;
    }

    /**
     * 解析批量测试数据生成语句
     * @param string $sqlstmt
     * @return string
     * @version 2017-11-24
     */
    public static function parseSeedSqlStatement($sqlstmt)
    {
        $table = ''; // 测试数据表
        $count = 5; // 默认生成 5 条数据
        $start = 1; // 起始序号
        $sqlstmtcount = 0;
        $testdatafields = []; // 测试数据所有字段定义
        $sqlstmts = explode("\r", $sqlstmt); // textarea 回车符号是 \r，和 OS 平台无关
        // 逐行分析字段
        foreach ($sqlstmts as $sqlstmtone) {
            $sqlstmtone = trim($sqlstmtone);
            $pos = strpos($sqlstmtone, '// ');
            if (0 === $pos) {
                continue; // 删除注释
            }
            if ($pos) {
                $sqlstmtone = substr($sqlstmtone, 0, $pos); // 删除注释
            }
            if ($sqlstmtcount < 1) {
                $rowarr = explode(' ', $sqlstmtone, 2); // 第一行格式务必 "testdata {tablename},{count},{start}"
                $table = $rowarr[1];
                $tabletmparr = explode(',', $table);
                switch (count($tabletmparr)) {
                    case 3:
                        list($table, $count, $start) = $tabletmparr;
                        break;
                    case 2:
                        list($table, $count) = $tabletmparr;
                        break;
                }
                $sqlstmtcount = 1;
                continue;
            }
            $rowarr = explode('=', $sqlstmtone, 2); // 字段定义格式务必 "field=value"，一个一行
            $value = $rowarr[1];
            $testdatafields[$rowarr[0]] = $value;
            $sqlstmtcount = 1;
        }
        $testdatavaluesarr = [];
        for ($i = 0; $i < $count; $i++) {
            // 逐行分析
            // 支持函数: {i}, {rand(min,max)},{rang(1,3,5)}, {time()}, {date('Y-m-d H:i:s')}
            $valuearr = [];
            foreach ($testdatafields as $value) {
                if (false !== strpos($value, '{i}')) {
                    $value = str_replace('{i}', $start + $i, $value);
                }
                if (preg_match("/\{rand\(([\d]+),([\d]+)\)\}/", $value, $matches)) {
                    // 随机数：{rand(min,max)}, 如 测试{rand(1,10)}
                    $value = str_replace($matches[0], rand($matches[1], $matches[2]), $value);
                }
                if (preg_match("/\{rang\((.+)\)\}/", $value, $matches)) {
                    // 范围值：{rang(v1,v2,v3)}, 如 测试{rang(A,B,C)}
                    $fnvaluearr = explode(',', $matches[1]);
                    $value = str_replace($matches[0], $fnvaluearr[array_rand($fnvaluearr)], $value);
                }
                if (preg_match("/\{time\(\)\}/", $value, $matches)) {
                    // 时间戳：{time()}, 如 测试{time()}
                    $value = str_replace($matches[0], time(), $value);
                }
                if (preg_match("/\{date\((.+)\)\}/", $value, $matches)) {
                    // 日期时间：{date('Y-m-d H:i:s')}, 如 测试{date('Y-m-d H:i:s')}
                    $value = str_replace($matches[0], date($matches[1]), $value);
                }
                $valuearr[] = $value;
            }
            $testdatavaluesarr[] = "(" . implode(",", $valuearr) . ")";
        }
        $sqlstmt = 'insert into ' . $table . ' (`' . implode('`, `', array_keys($testdatafields)) . '`) values '
            . implode(',', $testdatavaluesarr) . ';';
        return $sqlstmt;
    }
    /**
     * 解析快速查询命令
     * @param string $tablename 数据表名称
     * @param array $fastsqls 快速命令配置内容
     * @return array 返回解析后的快速命令
     */
    public static function parseFastSqls($tablename, $fastsqls)
    {
        $quickquerysqls = [];
        if (!empty($fastsqls['-group-'])) {
            foreach ($fastsqls['-group-'] as $key => $val) {
                if (in_array($tablename, $val)) {
                    $quickquerysqls = array_key_exists($key, $fastsqls) ? $fastsqls[$key] : [];
                    break;
                }
            }
        }
        if (empty($quickquerysqls) && array_key_exists($tablename, $fastsqls)) {
            $quickquerysqls = $fastsqls[$tablename];
        }
        return $quickquerysqls;
    }
    /**
     * 获取示例语句
     * @param string $action
     * @return string
     * @version 1.0 2017-10-16
     */
    public static function getDefaultExampleSql($action, $isComment = true)
    {
        $sqlstmt = '';
        $egsqlarr = [];
        switch ($action) {
            case 'egcreate_database':
                $egsqlarr = [
                    'CREATE DATABASE IF NOT EXISTS db_test DEFAULT CHARSET utf8 COLLATE utf8_general_ci;',
                    'CREATE DATABASE IF NOT EXISTS db_test DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;',
                    '-- ----',
                    '-- utf8 和 utf8mb4 区别：',
                    '-- 1. utf8 一个字符占用 3 个字节，utf8mb4 一个字符占用 4 个字节，但实际应用中，数据库体积差距可以忽略不计',
                    '-- 2. utf8 不支持 emoji 表情，utf8mb4 支持 emoji 表情',
                    '-- 3. 在排序和校对规则方面，utf8_unicode_ci 比较准确，utf8_general_ci 速度比较快',
                    '-- 4. 如果数据表使用 utf8mb4 编码，数据库也要使用 utf8mb4 编码',
                    '-- 5. 字段的最大长度是 767 字节，utf8 编码可以支持 255 个字符（255*3=765），utf8mb4 只能支持 191 个'
                        . '字符（191*4bit=764）',
                    '-- 6. 建议普通表使用 utf8，如果这个表需要支持 emoji 表情符号就使用 utf8mb4',
                    '-- 7. 包含 emoji 表情符号的字段需设置为编码为 utf8mb4_unicode_ci，字段最长为 191 个字符，否则会报错：'
                        . 'Specified key was too long; max key length is 767 bytes',
                    "-- -- 例如 `name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '';"
                ];
                break;
            case 'egcreate_table':
                $egsqlarr = array(
                    "drop table if exists `t_test`;",
                    "create table if not exists `t_test` (",
                    "   id int auto_increment primary key comment '主键编号',",
                    "   `name` varchar(32) not null default '' comment '名称',",
                    "   `content` varchar(20480) not null default '' comment '内容',",
                    "   user_id int not null default 0 comment '用户编号',",
                    "   ordinal smallint not null default 0 comment '排列次序',",
                    "   `status` smallint not null default 0 comment '状态:0=未审核,1=正常,4=关闭',",
                    "   created_time timestamp not null default CURRENT_TIMESTAMP comment '创建时间',",
                    "   PRIMARY KEY(id),",
                    "   UNIQUE  (user_id, `name`),",
                    "   FOREIGN KEY (category_id) REFERENCES t_category(id)",
                    "   CONSTRAINT `fk_test_category_id` FOREIGN KEY (`category_id`) REFERENCES `t_category` (`id`),",
                    "   INDEX idx_userid (user_id)",
                    ") comment 'test表[v1.0 " . date('Y-m-d') . " by author]' ENGINE=InnoDB default CHARSET=utf8;",
                );
                break;
            case 'egcreate_view':
                $egsqlarr = array(
                    "drop view if exists `v_test`;",
                    "create or replace view v_test as select t1.*,t2.colname from t_test1 t1 left join t_test2 t2 "
                        . "on t1.id=t2.fkid order by t1.id desc;",
                );
                break;
            case 'egcreate_function':
                $egsqlarr = array(
                    "DELIMITER ;;",
                    "drop function if exists fn_indexOf;;",
                    "create function fn_indexOf",
                    "(",
                    "    -- 要分割的字符串",
                    "    pstr_text varchar(20480),",
                    "    -- 分隔符号",
                    "    pstr_splitor varchar(10),",
                    "    -- 取第几个元素",
                    "    pint_index int",
                    ")",
                    "returns varchar(1024)",
                    "begin",
                    "    declare vint_location int;",
                    "    declare vint_start int;",
                    "    declare vint_next int;",
                    "    declare vint_seed int;",
                    "    set pstr_text=ltrim(rtrim(pstr_text));",
                    "    set vint_start=1;",
                    "    set vint_next=1;",
                    "    set vint_seed=length(pstr_splitor);",
                    "    set vint_location=position(pstr_splitor in pstr_text);",
                    "    while vint_location<>0 and pint_index>vint_next do",
                    "        set vint_start=vint_location+vint_seed;",
                    "        set vint_location=locate(pstr_splitor,pstr_text,vint_start);",
                    "        set vint_next=vint_next+1;",
                    "    end while;",
                    "    if vint_location =0 THEN",
                    "        set vint_location =length(pstr_text)+1;",
                    "    end if;",
                    "    -- 这里存在两种情况：1、字符串不存在分隔符号 2、字符串中存在分隔符号，跳出 while 循环后，"
                        . "vint_location为0，那默认为字符串后边有一个分隔符号。",
                    "    set pstr_text=substring(pstr_text,vint_start,vint_location-vint_start);",
                    "    return pstr_text;",
                    "end;;",
                    "DELIMITER ;",
                );
                $sqlstmt = "/**\r获取数组中指定位置的元素\r如果超过界限则返回最后界限的值\r@version 1.0 2014-11-12\r"
                    . "调用示例：select func_indexOf('8,9,4',',',2)\r返回值：9\r*/\r";
                break;
            case 'egcreate_procedure':
                $egsqlarr = array(
                    "DELIMITER ;;",
                    "drop procedure if exists proc_array_test;;",
                    "create procedure proc_array_test(pstr_numbers text)",
                    "begin",
                    "    declare vstr_item varchar(255);",
                    "    declare vint_next int;",
                    "    set vint_next=1;",
                    "    while vint_next<=func_count_split(pstr_numbers,'+') do",
                    "        set vstr_item = func_indexOf(pstr_numbers,'+',vint_next);",
                    "        set vstr_item=REPLACE(vstr_item,':',' and phoneNumber=\'');",
                    "        set vstr_item=CONCAT('update t_cti_call_plan set state=1 where taskID=',vstr_item,'\';');",
                    "        set @vstr_sql=vstr_item;",
                    "        prepare stmt from @vstr_sql;",
                    "        execute stmt;",
                    "        deallocate prepare stmt;",
                    "       set vint_next=vint_next+1;",
                    "    end while;",
                    "end;;",
                    "DELIMITER ;",
                );
                $sqlstmt = "/**\r* 批量更新号码状态\r* @version 1.0 2014-11-1 by author\r* @example call proc_array_test"
                    . "('1212:13950076987+1211:13950076987+1210:13950076987');\r*/\r";
                break;
            case 'egcreate_trigger':
                $egsqlarr = array(
                    "delimiter ;;",
                    "create trigger trg_update_test after update on t_test for each row",
                    "begin",
                    "    call proc_update_test(t_test.id,t_test.status);",
                    "end;;",
                    "delimiter ;",
                    "--",
                    "DELIMITER ;;",
                    "create trigger trg_insert_user alter insert on t_user for each row",
                    "begin",
                    "    insert into t_finance_account (userID) values (t_user.id);",
                    "end;;",
                    "DELIMITER ;",
                );
                break;
            case 'egcreate_user':
                $egsqlarr = array(
                    '-- 创建用户同时授权',
                    "GRANT ALL PRIVILEGES ON databasename.tablename TO 'username'@'host' IDENTIFIED BY '123456';",
                    "GRANT ALL PRIVILEGES ON `db_test`.* TO 'db_user'@'localhost' IDENTIFIED BY '123456';",
                    "FLUSH PRIVILEGES;",
                    "-- 创建用户",
                    "CREATE USER 'username'@'host' IDENTIFIED BY 'password';",
                    "eg. CREATE USER 'db_user'@'127.0.0.1' IDENTIFIED BY '123456';",
                    "-- 授权用户",
                    "GRANT privileges ON databasename.tablename TO 'username'@'host'",
                    "eg. GRANT SELECT, INSERT ON `db_test`.* TO 'db_user'@'localhost';",
                    '-- 设置与更改用户密码',
                    "SET PASSWORD FOR 'username'@'host' = PASSWORD('newpassword');",
                    '-- 撤销用户权限',
                    "REVOKE privilege ON databasename.tablename FROM 'username'@'host';",
                    "eg. REVOKE SELECT ON mq.* FROM 'dog2'@'localhost';",
                    '-- 删除用户',
                    "DROP USER 'username'@'host';",
                    "eg. DROP USER 'db_user'@'127.0.0.1';",
                    '-- 查看用户的授权',
                    "SHOW GRANTS FOR 'username'@'host';",
                    "eg. SHOW GRANTS FOR db_user@127.0.0.1;",
                );
                break;
            case 'egexport_import':
                $egsqlarr = array(
                    '-- 导出整个数据库的表结构，包含数据',
                    'eg. mysqldump -uroot -proot --add-drop-table db_test > ./db_test.sql',
                    '-- 只导出整个数据库的表结构，不含数据（-d 等同于 --no-data）',
                    'eg. mysqldump -uroot -proot -d --add-drop-table db_test > ./db_test.sql',
                    '-- 只导出整个数据库数据，不包含表结构（-t 等同于 --no-create-info）',
                    'eg. mysqldump -uroot -proot -t db_test > ./db_test.sql',
                    '-- 导出整个数据库的表结构和存储过程、自定义函数，不含数据（-R 等同于 --routines）',
                    'eg. mysqldump -uroot -proot -d --add-drop-table -R db_test > ./db_test_routines.sql',
                    '-- 导出指定的数据表的表结构和数据',
                    'eg. mysqldump -uroot -proot db_test t_test1 t_test2 > ./db_test.test1n2.sql',
                    '-- 将 h1 服务器中的 db_test 数据库导入到 h2 中的 db_test 数据库中，h2 的 db_test 数据库必须存在否则'
                        . '会报错（-c参数表示压缩数据）',
                    'eg. mysqldump -h127.0.0.1 -uroot -proot -c db_test | mysql -h127.0.0.1 -uroot -proot db_test',
                    '-- 导入数据',
                    'eg. use db_test;',
                    'eg. source ./db_test.sql;',
                );
                break;
        }
        if ($egsqlarr) {
            $sqlstmt .= $isComment ? ('-- ' . implode("\r-- ", $egsqlarr)) : (implode(PHP_EOL, $egsqlarr));
        }
        return $sqlstmt;
    }

    /**
     * 获取数据操作示例语句
     * @param string $action
     * @param string $curdbname
     * @return string
     * @version 1.0 2017-10-16
     */
    public static function getDbExampleSql($action, $curdbname)
    {
        $sqlstmt = '';
        switch ($action) {
            case 'dbvariables':        // 查看数据库变量信息
                // $sqlstmt = 'select VARIABLE_NAME, VARIABLE_VALUE FROM information_schema.global_variables;';
                $sqlstmt = 'show global variables;';
                break;
            case 'dbstatus':
                $sqlstmt = 'show status;';
                break;
            case 'dbcreate':        // 查看创建数据库命令
                $sqlstmt = 'show create database ' . $curdbname . ';';
                break;
            case 'dbschema':        // 查看创建数据库命令
                $sqlstmt = 'select TABLE_NAME, ENGINE, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH, AUTO_INCREMENT, '
                    . 'CREATE_TIME, UPDATE_TIME, TABLE_COLLATION, TABLE_COMMENT from information_schema.TABLES where '
                    . "information_schema.TABLES.TABLE_SCHEMA = '$curdbname';";
                break;
        }
        return $sqlstmt;
    }

    /**
     * 获取数据表示例语句
     * @param string $action
     * @param string $curdbname
     * @param string $curtable
     * @param array $fields
     * @param array $columns
     * @return string
     * @version 1.0 2017-10-16
     * @version 1.1 2017-11-24 新增测试数据示例语句功能
     */
    public static function getTableExampleSql($action, $curdbname, $curtable, $fields, $columns = [])
    {
        $sqlstmt = '';
        switch ($action) {
            case 'egmeta':
                $sqlstmt = 'SELECT `TABLE_CATALOG`,`TABLE_SCHEMA`,`TABLE_NAME`,`TABLE_TYPE`,`ENGINE`,`VERSION`,'
                    . '`ROW_FORMAT`,`TABLE_ROWS`,`AVG_ROW_LENGTH`,`DATA_LENGTH`,`MAX_DATA_LENGTH`,`INDEX_LENGTH`,'
                    . '`DATA_FREE`,`AUTO_INCREMENT`,`CREATE_TIME`,`UPDATE_TIME`,`CHECK_TIME`,`TABLE_COLLATION`,'
                    . '`CHECKSUM`,`CREATE_OPTIONS`,`TABLE_COMMENT` FROM `information_schema`.`TABLES` where '
                    . 'TABLE_SCHEMA=\'' . $curdbname . '\'and TABLE_NAME=\'' . $curtable . '\';';
                break;
            case 'egselect':
                if ($fields) {
                    $sqlstmt = 'select `' . implode('`, `', $fields) . '` from ' . $curtable . ' order by ' . $fields[0]
                        . ' desc limit 50;';
                }
                break;
            case 'egupdate':
                if ($fields) {
                    $sqlstmt = 'eg. update ' . $curtable . ' set `' . implode('`=?, `', $fields) . '`=? where '
                        . $fields[0] . '=?;';
                }
                break;
            case 'eginsert':
                $sqlstmt = 'eg. insert into ' . $curtable . ' (`' . implode('`, `', $fields) . '`) values (:'
                    . implode(', :', str_replace('`', '', $fields)) . ');';
                break;
            case 'egdelete':
                if ($fields) {
                    $sqlstmt = 'eg. delete from ' . $curtable . ' where ' . $fields[0] . '=?;';
                }
                break;
            case 'egdesc': // desc命令
                $sqlstmt = "desc $curtable;";
                break;
            case 'egstructure': // 查看表结构
                $sqlstmt = 'select COLUMN_NAME, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, '
                    . 'NUMERIC_SCALE, EXTRA, COLUMN_COMMENT from information_schema.columns WHERE TABLE_SCHEMA='
                    . "'$curdbname' AND TABLE_NAME='$curtable';";
                break;
            case 'egindex': // 查看索引
                $sqlstmt = "show index from $curtable;";
                break;
            case 'egtriggers': // 查看触发器
                $sqlstmt = "show triggers like '$curtable';";
                break;
            case 'egalter': // alter命令
                $egsqlarr = array(
                    '-- 重命名表',
                    "eg. alter table `$curtable` rename to `$curtable`",
                    '-- 新增列',
                    "eg. alter table `$curtable` add `id` int comment 'id 列的备注信息';",
                    "eg. alter table `$curtable` add `colname1` varchar(255) comment 'colname1 列的备注信息';",
                    '-- 重命名列名',
                    "eg. alter table `$curtable` change colname1 colname2 varchar(255) comment '修改了 colname1 的列名为"
                        . "colname2 并增加备注信息';",
                    '-- 修改列的数据类型',
                    "eg. alter table `$curtable` modify colname2 varchar(32) comment 'colname2 的备注信息';",
                    "eg. alter table `$curtable` change colname2 colname2 varchar(64) not null;",
                    '-- 添加主键约束',
                    "eg. alter table `$curtable` add primary key(id);",
                    '-- 删除主键约束',
                    "eg. alter table `$curtable` drop primary key;",
                    '-- 添加外键约束',
                    "eg. alter table `$curtable` add foreign key (category_id) references t_category(id);",
                    '-- 解除外键约束',
                    "eg. alter table `$curtable` drop foreign key category_id",
                    '-- 删除外键约束（alter table 从表 add constraint 外键（FK_从表_主表）foreign key （从表外键字段） '
                        . 'references 主表（主键字段））',
                    "eg. alter table `$curtable` drop fk_test_category_id",
                    '-- 添加索引',
                    "eg. alter table `$curtable` add index idx_name1_name2 (colname1,colname2);",
                    '-- 删除索引',
                    "eg. alter table `$curtable` drop index idx_name1_name2;",
                    '-- 加唯一限制条件的索引',
                    "eg. alter table `$curtable` add unique idxu_colname2 (colname2);",
                );
                $sqlstmt = implode("\r", $egsqlarr);
                unset($egsqlarr);
                break;
            case 'egcreate': // 创建表
                $sqlstmt = "show create table $curtable;";
                break;
            case 'egduplicate': // 复制表
                $sqlstmt = 'eg. insert into table2 select * from table1;' . PHP_EOL;
                $sqlstmt .= 'eg. insert into table2 (field1,field2) select field1,field2 from table1;' . PHP_EOL;
                $sqlstmt .= 'eg. create table table2(select * from table1);' . PHP_EOL;
                $sqlstmt .= '-- -- mysql 不支持 select * into table2 from table 这种复制命令';
                break;
            case 'egtruncate': // truncate命令
                $sqlstmt = "eg. truncate table $curtable;";
                break;
            case 'egcount': // count命令
                $sqlstmt = "select count(*) from $curtable;";
                break;
            case 'egdrop':
                $sqlstmt = "eg. drop table if exists $curtable;";
                break;
            case 'egtestinsert':
                // COLUMN_NAME, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE,
                // EXTRA, COLUMN_COMMENT
                $sqlstmt = 'eg. testdata ' . $curtable . ",10,1\r";
                $sqlstmt .= "// 第一行格式务必为：testdata {数据表名},{生成数量},{序号起始值}\r";
                $sqlstmt .= "// 字段定义每个一行，格式为：field=value\r";
                $sqlstmt .= "// 字段值最多可包含一个函数，支持的函数如下：\r";
                $sqlstmt .= "// 序号：{i}，随机数：{rand(min,max)}，范围值：{rang(A,B,C)}，时间戳：{time()}，时间："
                    . "{date('Y-m-d H:i:s')}\r";
                foreach ($columns as $column) {
                    if ($column['IS_NULLABLE'] == 'YES') {
                        continue; // 允许null的直接跳过
                    }
                    $field = $column['COLUMN_NAME'];
                    if ($column['DATA_TYPE'] == 'varchar' || $column['DATA_TYPE'] == 'char') {
                        $comment = ' // [' . $column['DATA_TYPE'] . '(' . $column['CHARACTER_MAXIMUM_LENGTH'] . ')] '
                            . $column['COLUMN_COMMENT'];
                    } else {
                        $comment = ' // [' . $column['DATA_TYPE'] . '] ' . $column['COLUMN_COMMENT'];
                    }
                    $comment = ' // [' . $column['DATA_TYPE'] . ($column['DATA_TYPE'] == 'varchar' ? '('
                        . $column['CHARACTER_MAXIMUM_LENGTH'] . ')' : '') . '] ' . $column['COLUMN_COMMENT'];
                    switch ($column['DATA_TYPE']) {
                        case 'int':
                        case 'integer':
                        case 'tinyint':
                        case 'smallint':
                        case 'bigint':
                        case 'decimal':
                        case 'float':
                        case 'real':
                        case 'double':
                            if ($field == 'id') {
                                $value = '{i}';
                            } else {
                                if (false !== strpos($field, 'time')) {
                                    $value = '{time()}';
                                } elseif (
                                    false !== strpos($field, 'status') ||
                                    false !== strpos($field, 'state') ||
                                    false !== strpos($field, 'sex')
                                ) {
                                    $value = '{rang(0,1,2)}';
                                } else {
                                    $value = '{rand(1,100)}';
                                }
                            }
                            break;
                        case 'datetime':
                        case 'date':
                        case 'time':
                        case 'timestamp':
                            $value = "{date('Y-m-d H:i:s')}";
                            break;
                        default:
                            if (false !== strpos($field, 'name') || false !== strpos($field, 'title')) {
                                $value = "'{$field}_{i}'";
                            } else {
                                $value = "'{$field}_{rand(1,100)}'";
                            }
                            break;
                    }
                    $sqlstmt .= $field . '=' . $value . $comment . "\r";
                }
                break;
        }
        return $sqlstmt;
    }

    /**
     * 获取视图示例语句
     * @param string $action
     * @param string $curdbname
     * @param string $curview
     * @param array $fields
     * @return string
     * @version 1.0 2017-10-16
     */
    public static function getViewExampleSql($action, $curdbname, $curview, $fields)
    {
        if (!$fields) {
            return false;
        }
        $sqlstmt = '';
        switch ($action) {
            case 'egselect':
                $sqlstmt = 'select `' . implode('`, `', $fields) . '` from ' . $curview . ' order by ' . $fields[0]
                    . ' desc limit 50;';
                break;
            case 'egupdate':
                if ($fields) {
                    $sqlstmt = 'eg. update ' . $curview . ' set `' . implode('`=?, `', $fields) . '`=? where '
                        . $fields[0] . '=?;';
                }
                break;
            case 'eginsert':
                $sqlstmt = 'eg. insert into ' . $curview . ' (`' . implode('`, `', $fields) . '`) values (:'
                    . implode(', :', str_replace('`', '', $fields)) . ');';
                break;
            case 'egdelete':
                $sqlstmt = 'eg. delete from ' . $curview . ' where ' . $fields[0] . '=?;';
                break;
            case 'egstructure':
                $sqlstmt = 'select COLUMN_NAME, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, '
                    . 'NUMERIC_SCALE, EXTRA, COLUMN_TYPE, COLUMN_KEY, COLUMN_COMMENT from information_schema.columns '
                    . "WHERE TABLE_SCHEMA='$curdbname' AND TABLE_NAME='$curview';";
                break;
            case 'egalter':
                $sqlstmt = "eg. create or replace view $curview as ...;";
                break;
            case 'egcreate':
                $sqlstmt = 'show create view ' . $curview . ';';
                break;
        }
        return $sqlstmt;
    }

    /**
     * 获取程序示例语句
     * @param string $action
     * @param string $curroutine
     * @param string $curroutinetype
     * @param array $routineParamNames
     * @return string
     * @version 1.0 2017-10-16
     */
    public static function getRoutineExampleSql($action, $curroutine, $curroutinetype, $routineParamNames)
    {
        $sqlstmt = '';
        switch ($action) {
            case 'egcall':
                $sqlstmt = 'eg. ' . ($curroutinetype == 'function' ? 'select ' : 'call ') . $curroutine . '('
                    . ($routineParamNames ? implode(', ', $routineParamNames) : '') . ');';
                break;
            case 'egdrop':
                $sqlstmt = 'eg. drop ' . $curroutinetype . ' if exists ' . $curroutine . ';';
                break;
            case 'egcreate':
                $sqlstmt = 'show create ' . $curroutinetype . ' ' . $curroutine . ';';
                break;
        }
        return $sqlstmt;
    }
}

/**
 * Html页面助手类
 */
class HtmlHelper
{
    static $uiResource = [
        'provider' => 'local',
        'css' => [
            '_tpl' => '<link href="%s" rel="stylesheet" />',
            'bootstrap' => [
                'cdn' => 'https://cdn.bootcss.com/twitter-bootstrap/4.4.1/css/bootstrap.min.css',
                'local' => '/assets/dist/css/adminlte.min.css'
            ]
        ],
        'javascript' => [
            '_tpl' => '<script src="%s"></script>',
            'jquery' => [
                'cdn' => 'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js',
                'local' => '/assets/dist/plugins/jquery/jquery.min.js'
            ],
            'bootstrap' => [
                'cdn' => 'https://cdn.bootcss.com/twitter-bootstrap/4.4.1/js/bootstrap.bundle.min.js',
                'local' => '/assets/dist/plugins/bootstrap/bootstrap.bundle.min.js'
            ]
        ]
    ];
    public static function setUiResource($map)
    {
        self::$uiResource = array_merge(self::$uiResource, $map);
    }

    public static function getResourceLink($names, $type = 'css')
    {
        $maps = self::$uiResource;
        if (!$names || !$type || !$maps) {
            return '';
        }
        if (!array_key_exists($type, $maps)) {
            return '';
        }
        if (!is_array($names)) {
            $names = [$names];
        }
        $uri = '';
        $provider = $maps['provider'];
        $map = $maps[$type];
        $content = '';
        foreach ($names as $name) {
            if (empty($map[$name])) {
                continue;
            }
            $uri = empty($map[$name][$provider]) ? false : $map[$name][$provider];
            if (!$uri) {
                foreach ($map[$name] as $varUrl) {
                    if ($varUrl) {
                        $uri = $varUrl;
                        break;
                    }
                }
                if (!$uri) {
                    continue;
                }
            }
            $content .= sprintf($map['_tpl'], $uri) . PHP_EOL;
        }
        return $content;
    }
    /**
     * 获取用户输入
     * @param string $name
     * @param mixed $defv
     * @param integer $filter
     * @param boolean $istrim
     * @return mixed
     */
    public static function input($name, $defv = '', $filter = FILTER_SANITIZE_STRING, $trim = true)
    {
        if (isset($_REQUEST[$name])) {
            $defv = $filter ? filter_var($_REQUEST[$name], FILTER_SANITIZE_STRING) : $_REQUEST[$name];
            if ($trim) {
                $defv = trim($defv);
            }
        }
        return $defv;
    }
    /**
     * 设置用户导出下载的浏览器头部信息
     * @param boolean $isexcel 是否 Excel 格式
     * @param string $filename 文件名
     */
    public static function setExportHeader($isexcel, $filename)
    {
        if ($isexcel) {
            header("Content-Type: application/vnd.ms-excel; name='excel'");
        }
        header("Content-type:application/octet-stream");
        header('Content-Disposition:attachment; filename=' . $filename);
    }
    /**
     * 获取验证码
     * @return string
     */
    public static function getCaptcha()
    {
        $type = rand(1, 3);
        $tip = '<span style="color:#F00; margin-left:10px;"><strong id="error"></strong>（注意：生产环境才会出现验证码，'
            . '请务必谨慎操作！做好数据备份！）</span>';
        switch ($type) {
            case 1:
                $a = rand(1, 150);
                $b = rand(150, 100);
                $val = $b - $a;
                return '<input type="text" name="captcha" placeholder="' . $val . '" title="' . $val
                    . '" required="required" style="width:3em" />+' . $a . '=' . $b . $tip;
            case 2:
                $a = rand(1, 100);
                $b = rand(100, 1000);
                $val = $b - $a;
                return $a . '+<input type="text" name="captcha" placeholder="' . $val . '" title="' . $val
                    . '" required="required" style="width:3em" />=' . $b . $tip;
            case 3:
                $a = rand(1, 150);
                $b = rand(1, 150);
                $val = $a + $b;
                return $a . '+' . $b . '=<input type="text" name="captcha" placeholder="' . $val . '" title="' . $val
                    . '" required="required" style="width:3em" />' . $tip;
        }
    }
    /**
     * 显示侧边栏数据库操作按钮
     */
    public static function showAsideDbOptionButtons()
    {
        global $curenv, $curdbname, $token;
        $url = '?env=' . $curenv . '&db=' . $curdbname . '&token=' . $token . '&action=';
        echo '<div class="btn-group-vertical btn-group-sm w-100">';
        echo '<a class="btn btn-light text-left" title="查看 ', $curdbname, ' 数据库变量信息（show global variables）" ';
        echo 'href="', $url, 'dbvariables">查看数据库变量信息</a>';
        echo '<a class="btn btn-light text-left" title="查看 ', $curdbname, ' 数据库变量信息（show status）" ';
        echo 'href="', $url, 'dbstatus">查看数据库状态信息</a>';
        echo '<a class="btn btn-light text-left" title="查看 ', $curdbname, ' 数据库架构信息（information_schema.TABLES）" ';
        echo 'href="', $url, 'dbschema">查看数据库架构信息</a>';
        echo '<a class="btn btn-light text-left" title="查看 ', $curdbname, ' 创建数据库语句" href="', $url, 'dbcreate">';
        echo '查看数据库创建语句</a>';
        echo '<a class="btn btn-light text-left" title="查看 ', $curdbname, ' 所有数据表的模型" href="', $url, 'dbmodel" ';
        echo 'target="_blank">查看数据表模型</a>';
        echo '<a class="btn btn-light text-left" title="导出 ', $curdbname, ' 所有数据表结构到 Markdown 文件，以便于发布到';
        echo '在线文档系统" href="', $url, 'dbexportasmarkdown">导出表结构到 Markdown 文件</a>';
        echo '<a class="btn btn-light text-left" title="导出 ', $curdbname, ' 所有数据表结构到 Excel 文件，以便于快速模型设计';
        echo '或文件归档" href="', $url, 'dbexportasexcel">导出表结构到 Excel 文件</a>';
        echo '<a class="btn btn-light text-left" title="导出 ', $curdbname, ' 所有数据表结构到 HTML 文件，以便于快速浏览模型';
        echo '大纲" href="', $url, 'dbexportashtml">导出数据表结构到 Html 文件</a>';
        echo '<a class="btn btn-light text-left" title="导出 ', $curdbname, ' 所有数据表结构到 SQL 文件，以便于迁移数据库" ';
        echo 'href="', $url, 'dbexportassql">导出数据表结构到 SQL 文件</a>';
        echo '</div>';
    }

    /**
     * 显示数据表的操作示例按钮
     * @version 1.8 2017-10-16
     * @version 1.9 2017-11-23 新增data-sql功能
     */
    public static function showTableOptionButtons()
    {
        global $curenv, $curdbname, $curtable, $token;
        $url = '?env=' . $curenv . '&db=' . $curdbname . '&table=' . $curtable . '&token=' . $token . '&action=';

        echo '<div class="btngroup">';
        echo '<a class="btn btn-sm btn-info" href="', $url, '" title="在新窗口打开数据表" target="_blank">新窗口打开</a>';
        echo '<a class="btn btn-sm btn-info" title="查看表结构模型" href="', $url,
            'tableexportashtml" target="_blank">查看模型</a>';
        echo '<a class="btn btn-sm btn-info" title="导出表结构模型到 Excel 文件" href="', $url,
            'tableexportasexcel" target="_blank">导出模型到 Excel</a>';
        echo '<a class="btn btn-sm btn-info" title="导出创建表命令到 SQL 文件" href="', $url,
            'tableexportassql" target="_blank">导出创建表命令</a>';
        echo '<a class="btn btn-sm btn-info" title="导出与表结构相匹配的 Phinx Migrate 迁移文件" href="', $url,
            'exportasmigrate">导出 Phinx Migrate</a>';
        echo '<a class="btn btn-sm btn-info" title="导出与表结构相匹配的 Phinx Seed 测试数据文件" href="', $url,
            'exportasseed">导出 Phinx Seed</a>';
        echo '</div><div class="btngroup" style="margin:5px 0">';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egselect" title="查询记录语句示例">SELECT</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egcount" title="查询表记录数量语句示例">COUNT</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egupdate" title="更新记录语句示例">UPDATE</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'eginsert" title="插入记录语句示例">INSERT</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egdelete" title="删除记录语句示例">DELETE</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egalter" title="更改表结构语句示例">ALTER</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egcreate" title="创建表结构语句示例">CREATE</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egduplicate" title="复制表语句示例">SELECT INTO</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egtruncate" title="清空表记录语句示例">TRUNCATE</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egdrop" title="丢弃表结构语句示例">DROP</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egdesc" title="查看表结构语句示例">DESC</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egstructure" title="查看表结构语句示例">STRUCTURE</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egmeta" title="查询数据表元数据语句示例">META</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egindex" title="查看表索引语句示例">INDEX</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egtriggers" title="查看表触发器语句示例">TRIGGERS</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egtestinsert" title="批量生成测试数据">TEST DATA</a>';
        echo '</div>';
    }

    /**
     * 显示视图的操作示例按钮
     * @param array $fields
     * @version 1.8 2017-10-16
     * @version 1.9 2017-11-23 新增data-sql功能
     */
    public static function showViewOptionButtons($fields)
    {
        global $curenv, $curdbname, $curview, $token;
        $url = '?env=' . $curenv . '&db=' . $curdbname . '&view=' . $curview . '&token=' . $token . '&action=';
        echo '<span class="btngroup">';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egselect" title="查询记录语句示例">SELECT</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egupdate" title="更新记录语句示例" data-sql="',
            SqlHelper::getViewExampleSql('egupdate', $curdbname, $curview, $fields),
            '" class="btn_example_sql">UPDATE</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'eginsert" title="插入记录语句示例" data-sql="',
            SqlHelper::getViewExampleSql('eginsert', $curdbname, $curview, $fields),
            '" class="btn_example_sql">INSERT</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egdelete" title="删除记录语句示例" data-sql="',
            SqlHelper::getViewExampleSql('egdelete', $curdbname, $curview, $fields),
            '" class="btn_example_sql">DELETE</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egstructure" title="查看视图结构语句示例">STRUCTURE</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egalter" title="更改视图结构语句示例" data-sql="',
            SqlHelper::getViewExampleSql('egalter', $curdbname, $curview, $fields),
            '" class="btn_example_sql">ALTER</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egcreate" title="创建视图结构语句示例">CREATE</a>';
        echo '</span>';
    }

    /**
     * 显示程序的操作示例按钮
     * @param array $routineParamNames
     * @version 1.8 2017-10-16
     */
    public static function showRoutineOptionButtons($routineParamNames)
    {
        global $curenv, $curdbname, $curroutine, $curroutinetype, $token;
        $url = '?env=' . $curenv . '&db=' . $curdbname . '&routine=' . $curroutine . '&routinetype=' . $curroutinetype
            . '&token=' . $token;
        echo '<span class="btngroup">';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, '&action=egcall" title="执行程序语句示例" data-sql="',
            SqlHelper::getRoutineExampleSql('egcall', $curroutine, $curroutinetype, $routineParamNames),
            '" class="btn_example_sql">CALL</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, '&action=egdrop" title="删除程序语句示例" data-sql="',
            SqlHelper::getRoutineExampleSql('egdrop', $curroutine, $curroutinetype, $routineParamNames),
            '" class="btn_example_sql">DROP</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, '&action=egcreate" title="创建程序结构语句示例">CREATE</a>';
        echo '</span>';
    }

    /**
     * 显示默认的操作示例按钮
     * @param string $token
     * @version 1.8 2017-10-16
     */
    public static function showDefaultOptionButtons()
    {
        global $curenv, $curdbname, $token;
        $url = '?env=' . $curenv . '&db=' . $curdbname . '&token=' . $token . '&action=';
        echo '<span class="btngroup">';
        echo '<a class="btn btn-sm btn-secondary" href="', $url,
            'egcreate_database" title="创建数据库语句示例">创建数据库</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url,
            'egcreate_table" title="创建数据表语句示例">创建数据表</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egcreate_view" title="创建视图语句示例">创建视图</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url,
            'egcreate_function" title="创建函数语句示例">创建函数</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url,
            'egcreate_procedure" title="创建存储过程语句示例">创建存储过程</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url,
            'egcreate_trigger" title="创建触发器语句示例">创建触发器</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url, 'egcreate_user" title="创建用户语句示例">创建用户</a>';
        echo '<a class="btn btn-sm btn-secondary" href="', $url,
            'egexport_import" title="数据库导入导出示例">导入导出</a>';
        echo '</span>';
    }

    /**
     * 获取sql编辑器的默认提示
     * @return string
     * @version 1.9 2017-10-16 Added.
     */
    public static function getSqlEditorPlaceholder()
    {
        $content_arr = array(
            '/*',
            '* ' . APPNAME . ' SQL编辑器',
            '* 支持功能：',
            '* &gt; 支持智能忽略单行注释、多行注释（点击编辑器 N 次自动去除注释）',
            '* &gt; 支持多语句批量执行，每条语句务必以分号结尾并以换行符号隔开',
            '* &gt; 支持存储过程等复杂语句，支持 {#object} 变量',
            '*/',
            '',
            '/* 以下为示例语句 */',
            '-- 多语句批量查询示例（每条语句务必以分号结尾并以换行符号隔开）',
            'insert into table1 values (1,2,3);',
            'select * from table1;',
        );
        return implode("\r", $content_arr);
    }

    /**
     * 显示数据表列表
     * @param array $list
     * @param string $tableprefix
     * @param boolean $isul
     * @param integer $curLen
     * @version 2017-10-20
     */
    public static function showAsideTableList($list, $tableprefix, $isul = true, $curLen = 20)
    {
        global $curenv, $curdbname, $curtable, $token, $config;
        if (!$list) {
            return;
        }
        $i = 1;
        $url = '';
        if ($isul) {
            echo '<ul class="list-unstyled">';
        } else {
            echo '<select class="form-control form-control-sm" id="slt-table" title="数据表跳转">';
            // 收藏夹
            if ($curdbname && isset($config[FAVORITE][$curdbname])) {
                $favorites = $config[FAVORITE][$curdbname];
                if ($favorites) {
                    echo '<option value="">==收藏夹==</option>';
                    foreach ($favorites as $objname) {
                        if ($tableprefix && (0 === strpos($objname, $tableprefix))) {
                            $title = substr($objname, strlen($tableprefix));
                        } else {
                            $title = $objname;
                        }
                        $url = '?env=' . $curenv . '&db=' . $curdbname . '&table=' . $objname
                            . '&action=egselect&token=' . $token;
                        echo '<option value="', $objname, '" title="', $objname, '" data-url="', $url, '">',
                            mysal_short_text($title, $curLen),
                            '</option>';
                    }
                }
            }
            echo '<option value="">==选择数据表==</option>';
        }
        $title = '';
        foreach ($list as $row) {
            $objname = $row['TABLE_NAME'];
            if ($tableprefix && (0 === strpos($objname, $tableprefix))) {
                $title =   substr($objname, strlen($tableprefix));
            } else {
                $title =  $objname;
            }

            $url = '?env=' . $curenv . '&db=' . $curdbname . '&table=' . $objname . '&action=egselect&token=' . $token;
            if ($isul) {
                echo '<li', ($curtable == $objname ? ' class="active"' : ''), '>';
                echo '<a href="', $url, '" title="', $objname, '">', $i, '. ', $title, '</a></li>';
            } else {
                echo '<option value="', $objname, '" title="', $objname, '" data-url="', $url, '"';
                echo ($objname == $curtable ? ' selected="selected"' : ''), '>', mysal_short_text($title, $curLen),
                    '</option>';
            }
            $i++;
        }
        echo $isul ? '</ul>' : '</select>';
    }

    /**
     * 显示视图列表
     * @param array $list
     * @param boolean $isul
     * @param integer $curLen
     */
    public static function showAsideViewList($list, $isul = true, $curLen = 16)
    {
        global $curenv, $curdbname, $curview, $token;
        if (!$list) {
            return;
        }
        $i = 1;
        $url = '';
        if ($isul) {
            echo  '<ul class="list-unstyled">';
        } else {
            echo '<select class="form-control form-control-sm" id="slt-view" title="视图跳转">',
                '<option value="">==选择视图==</option>';
        }
        foreach ($list as $row) {
            $objname = $row['TABLE_NAME'];
            $url = '?env=' . $curenv . '&db=' . $curdbname . '&view=' . $objname . '&action=egselect&token=' . $token;
            if ($isul) {
                echo '<li', ($curview == $objname ? ' class="active"' : ''), '><a href="', $url, '" title="', $objname;
                echo '">', $i, '. ', $objname, '</a></li>';
            } else {
                echo '<option value="', $objname, '" title="', $objname, '" data-url="', $url, '"';
                echo ($objname == $curview ? ' selected="selected"' : ''), '>', mysal_short_text($objname, $curLen),
                    '</option>';
            }
            $i++;
        }
        echo $isul ? '</ul>' : '</select>';
    }

    /**
     * 显示函数列表
     * @param string $routineType
     * @param array $list
     * @param boolean $isul
     * @param integer $curLen
     */
    public static function showAsideRoutineList($routineType, $list, $isul = true, $curLen = 16)
    {
        global $curenv, $curdbname, $curroutine, $token;
        if (!$list) {
            return;
        }
        $i = 1;
        $url = '';
        $routineTypeName = ($routineType == 'procedure' ? '存储过程' : '函数');
        if ($isul) {
            echo '<ul class="list-unstyled">';
        } else {
            echo '<select class="form-control from-control-sm" id="slt-' . $routineType . '" title="'
                . $routineTypeName . '跳转">';
            echo '<option value="">==选择' . $routineTypeName . '==</option>';
        }
        foreach ($list as $row) {
            $objname = $row['ROUTINE_NAME'];
            $url = '?env=' . $curenv . '&db=' . $curdbname . '&routine=' . $objname . '&routinetype='
                . strtolower($row['ROUTINE_TYPE']) . '&action=egcall&token=' . $token;
            if ($isul) {
                echo '<li', ($curroutine == $objname ? ' class="active"' : ''), '><a href="', $url, '" title="';
                echo $objname, '">', $i, '. ', $objname, '</a></li>';
            } else {
                echo '<option value="', $objname, '" title="', $objname, '" data-url="', $url, '"';
                echo ($objname == $curroutine ? ' selected="selected"' : ''), '>', mysal_short_text($objname, $curLen),
                    '</option>';
            }
            $i++;
        }
        echo $isul ? '</ul>' : '</select>';
    }

    /**
     * 显示属性表格
     * @param array $data
     * @param boolean $iscli
     */
    public static function showPropertyTable($data, $iscli = false)
    {
        if (!$data) {
            echo $iscli ? '没有查询到任何记录' : '<p>没有查询到任何记录!</p>';
            return;
        }
        $i = 0;
        if (!$iscli) {
            echo '<table class="table table-sm table-striped table-bordered table-hover table-responsive" ',
                'style="border:none">';
        }
        foreach ($data as $k => $v) {
            $i++;
            if ($iscli) {
                echo '# ', $i, ' ------------------------------------------------', PHP_EOL;
                echo $k, ' = ', $v, PHP_EOL;
            } else {
                echo '<tr><td>', $i, '</td><td>', $k, '</td>';
                echo '<td>', (is_null($v) ? '<span style="color:#AAA">NULL</span>' : htmlspecialchars($v)), '</td>';
                echo '</tr>';
            }
        }
        if (!$iscli) {
            echo '</table>';
        }
    }
    public static function showTableCli($list)
    {
        self::showTable($list, ['islite' => false, 'iscli' => true]);
    }
    public static function showTableWeb($list)
    {
        self::showTable($list, ['islite' => false, 'iscli' => false]);
    }
    public static function showTableWebLite($list)
    {
        self::showTable($list, ['islite' => true, 'iscli' => false]);
    }
    /**
     * 把数据集显示为表格
     * @param array $list
     * @param boolean $islite
     * @param boolean $iscli
     * @version 2017-12-15
     */
    public static function showTable($list, $option)
    {
        if (!$list) {
            echo $option['iscli'] ? '没有查询到任何记录' : '<p>没有查询到任何记录!</p>';
            return;
        }
        $defaultOption = ['islite' => false, 'iscli' => false];
        $option = array_merge($defaultOption, $option);
        extract($option);
        $i = 0;
        if (!$iscli) {
            echo '<table class="table table-sm table-striped table-bordered table-hover">';
        }
        foreach ($list as $row) {
            if ($i < 1 && !$iscli) {
                if ($islite) {
                    echo '<tr><th style="min-width:40px;">#</th><th>', implode('</th><th>', array_keys($row)),
                        '</th><tr>';
                } else {
                    $fragment = 'result' . $i;
                    echo '<tr><th style="min-width:40px;">#<a name="', $fragment, '" href="#', $fragment,
                        '" class="lnk-show" title="显示所有隐藏列">[+]</a></th><th>';
                    echo implode(
                        '<a href="#' . $fragment . '" class="lnk-hide" title="隐藏此列">[-]</a></th><th>',
                        array_keys($row)
                    );
                    echo '<a href="#' . $fragment . '" class="lnk-hide" title="隐藏此列">[-]</a></th><tr>';
                }
            }
            $i++;
            if ($iscli) {
                echo '# ', $i, ' ------------------------------------------------', PHP_EOL;
                foreach ($row as $k => $v) {
                    echo $k, ' = ', $v, PHP_EOL;
                }
            } else {
                echo '<tr><td>', $i, '</td>';
                foreach ($row as $td) {
                    echo '<td>', (is_null($td) ? '<span style="color:#AAA">NULL</span>' : htmlspecialchars($td)),
                        '</td>';
                }
                echo '</tr>';
            }
        }
        if (!$iscli) {
            echo '</table>';
        }
    }

    /**
     * 显示本地时间
     * @param string $str 消息模板
     */
    public static function showLocaltime($str)
    {
        $yeardays = date('L') ? 366 : 365;
        $yearweeks = ceil($yeardays / 7);
        $motto = [
            '生命的价值在于自己看得起自已，人生的意义在于努力进取。',
            '光阴易逝，年华不再，时间就是生命，不要留下遗憾！',
            '不走心的努力，都是在敷衍自己！',
            '人生路上，我们都是孤独的行者，真正能帮你的，永远只有你自己。',
            '努力是一种生活态度，与年龄无关。',
        ];
        $replaces = [
            '{{yearweeks}}' => ceil($yeardays / 7), // 一年总周数
            '{{yearweek}}' => intval(date('W')), // 一年中第几周
            '{{yeardays}}' => $yeardays, // 一年总天数
            '{{yearday}}' => date('z'), // 一年中第几天
            '{{remaindays}}' => $yeardays - date('z'), // 一年还剩多少天
            '{{remainweeks}}' => $yearweeks - date('W'), // 一年还剩多少周
            '{{weekday}}' => mb_substr('日一二三四五六', date('w'), 1), // 星期几
            '{{motto}}' => $motto[array_rand($motto)], // 格言
        ];
        echo str_replace(array_keys($replaces), array_values($replaces), $str);
    }
    /**
     * 展示数据库信息表格
     * @param array $dbvariables
     * @param array &$optimizes
     */
    public static function showDbVariablesTable($dbvariables, &$optimizes)
    {
        echo '<table class="table table-striped table-bordered table-hover table-sm"><tr>';
        $i = 0;
        $tmpvarvalue = '';
        $tmpvarname = '';
        foreach ($dbvariables as $row) {
            if ($i > 0 && ($i % 3 == 0)) {
                echo '</tr><tr>';
            }
            $tmpvarname = $row['Variable_name'];
            $tmpvarvalue = $row['Value'];
            echo '<th>', $tmpvarname, '</th>';
            echo '<td class="text-wrap text-break" style="max-width:15rem">';
            if (strpos($tmpvarname, '_size') || strpos($tmpvarname, '_packet')) {
                echo '<span title="' . $tmpvarvalue . '">' . FileHelper::formatBytes($tmpvarvalue) . '</span>';
            } else {
                echo $tmpvarvalue;
            }
            echo '</td>';
            switch ($tmpvarname) {
                case 'max_connections':
                    if (intval($tmpvarvalue) < 2000) {
                        $optimizes[] = [
                            'name' => $tmpvarname, 'value' => $tmpvarvalue, 'recommend' => 2000,
                            'remark' => '最大连接数量'
                        ];
                    }
                    break;
                case 'sort_buffer_size':
                    if ($tmpvarvalue < 8388608) {
                        $optimizes[] = [
                            'name' => $tmpvarname,
                            'value' => FileHelper::formatBytes($tmpvarvalue),
                            'recommend' => '8 MB',
                            'remark' => '排序缓冲，增加 sort_buffer_size 值可以加速 ORDER BY 或 GROUP BY 操作，和连接数成'
                                . '正比，比如 1000 连接数就是 8MB*1000=8GB 的内存使用量'
                        ];
                    }
                    break;
                case 'key_buffer_size':
                    if ($tmpvarvalue < 536870912) {
                        $optimizes[] = [
                            'name' => $tmpvarname, 'value' => FileHelper::formatBytes($tmpvarvalue),
                            'recommend' => '512 MB',
                            'remark' => '索引的缓冲区大小，对于内存在 4GB 左右的服务器来说，该参数可设置为 256 MB'
                        ];
                    }
                    break;
                case 'innodb_buffer_pool_size':
                    if ($tmpvarvalue < 536870912) {
                        $optimizes[] = [
                            'name' => $tmpvarname, 'value' => FileHelper::formatBytes($tmpvarvalue),
                            'recommend' => '512 MB',
                            'remark' => '只需要用 Innodb 的话则可以设置它高达 70-80% 的可用内存。如果你的数据量不大，并且不会'
                                . '暴增，那么无需设置的太大了'
                        ];
                    }
                    break;
                case 'innodb_thread_concurrency':
                    if ($tmpvarvalue < 32) {
                        $optimizes[] = [
                            'name' => $tmpvarname, 'value' => $tmpvarvalue, 'recommend' => '32',
                            'remark' => '在并发量大的实例上，增加这个值，可以降低 InnoDB 在并发线程之间切换的花销，以增加'
                                . '系统的并发吞吐量'
                        ];
                    }
                    break;
                case 'innodb_log_files_in_group':
                    if ($tmpvarvalue < 3) {
                        $optimizes[] = [
                            'name' => $tmpvarname, 'value' => $tmpvarvalue, 'recommend' => '3',
                            'remark' => '循环方式将日志文件写到多个文件。推荐设置为 3'
                        ];
                    }
                    break;
            }
            $i++;
        }
        echo '</tr></table>';
    }
    public static function startTable()
    {
        echo '<table class="table table-striped table-bordered table-hover table-sm">';
    }
    public static function endTable()
    {
        echo '</table>';
    }
}

/**
 * FileHelper 文件助手类
 * @version 2018-1-13 SoChishun Added.
 */
class FileHelper
{
    /**
     * 保存文件处理
     */
    public static function saveFileHandler()
    {
        $path = HtmlHelper::input('path');
        $content = HtmlHelper::input('file_content', '', null);
        $action = HtmlHelper::input('action');
        if (!$path || ('savefile' != $action)) {
            return;
        }
        $result = file_put_contents($path, $content);
        echo json_encode(['result' => $result ? 'success' : 'failure', 'path' => $path]);
        exit();
    }
    /**
     * 读取文件处理
     */
    public static function readFileHandler()
    {
        $path = HtmlHelper::input('path');
        $action = HtmlHelper::input('action');
        if (!$path || ('readfile' != $action)) {
            return;
        }
        echo file_get_contents($path);
        exit();
    }
    /**
     * 列出所有文件
     * @param string $dir 路径
     * @param boolean $recursive 是否递归
     * @param string $ext 过滤要保留的扩展名
     * @param string $prefix 过滤要跳过的文件名前缀
     */
    public static function scandir($dir, $recursive = false, $ext = '', $skipPrefix = '', $closure = null, $level = 0)
    {
        $data = [];
        $files = scandir($dir);
        foreach ($files as $file) {
            $filepath = $dir . '/' . $file;
            if (is_dir($filepath)) {
                if ('.' == $file || '..' == $file) {
                    continue;
                }
                // 如果递归就再循环下去
                if ($recursive) {
                    if ($closure instanceof \Closure) {
                        call_user_func_array($closure, [$file, $filepath, $level, false]);
                        self::scandir($filepath, $recursive, $ext, $skipPrefix, $closure, $level + 1);
                    } else {
                        $data[$file] = self::scandir($filepath, $recursive, $ext, $skipPrefix, $closure, $level + 1);
                    }
                }
                continue;
            }
            // 过滤扩展名和文件名前缀
            if (($ext && !strpos($file, $ext)) || ($skipPrefix && false === strpos($file, $skipPrefix))) {
                continue;
            }
            if ($closure instanceof \Closure) {
                // 匿名方法四个参数：file:文件名,filepath:文件路径,level:目录层级,isfile:是否文件
                call_user_func_array($closure, [$file, $filepath, $level, true]);
            } else {
                $data[$file] = $filepath;
            }
        }
        return $data;
    }
    /**
     * PHP 高效遍历文件夹（大量文件不会卡死，带文件名排序功能）
     * @param string $path 目录路径
     * @param array $skips 要忽略的文件路径集合
     * @param integer $deepth 扫描深度
     */
    public static function fastScanDirAndFiles($path = './', $skips = array(), $deepth = 0)
    {
        return self::fastScanDir($path, 0, true, $skips, $deepth);
    }
    /**
     * PHP 高效遍历文件夹（大量文件不会卡死，带文件名排序功能）
     * @param string $path 目录路径
     * @param array $skips 要忽略的文件路径集合
     * @param integer $deepth 扫描深度
     */
    public static function fastScanOnlyDir($path = './', $skips = array(), $deepth = 0)
    {
        return self::fastScanDir($path, 0, false, $skips, $deepth);
    }
    /**
     * PHP 高效遍历文件夹（大量文件不会卡死，带文件名排序功能）
     * @param string $path 目录路径
     * @param integer $level 目录深度层级
     * @param boolean $showfile 是否显示文件(否则只遍历显示目录)
     * @param array $skips 要忽略的文件路径集合
     * @param integer $deepth 扫描深度
     */
    private static function fastScanDir($path = './', $level = 0, $showfile = true, $skips = array(), $deepth = 0)
    {
        if (!file_exists($path) || ($deepth && $level > $deepth)) {
            return array();
        }
        $path = str_replace('//', '/', $path . '/');
        $file = new \FilesystemIterator($path);
        $filename = '';
        $icon = ''; // 树形层级图形
        if ($level > 0) {
            $icon = ('|' . str_repeat('--', $level));
        }
        $outarr = array();
        foreach ($file as $fileinfo) {
            $filename = iconv('GBK', 'utf-8', $fileinfo->getFilename()); // 解决中文乱码
            $filepath = $path . $filename;
            if ($fileinfo->isDir()) {
                if (!($skips && in_array($filepath . '/', $skips))) {
                    $outarr[$filename] = array('path' => $filepath, 'type' => 'dir', 'icon' => $icon);
                    $outarr[$filename]['children'] = self::fastScanDir($filepath, $level + 1, $showfile);
                }
                continue;
            }
            if ($showfile && !($skips && !in_array($filepath, $skips))) {
                $outarr[$filename] = array('path' => $filepath, 'type' => 'file', 'icon' => $icon);
            }
        }
        if ($outarr) {
            ksort($outarr);
        }
        return $outarr;
    }

    /**
     * 返回指定路径下的文件信息列表
     * @param string $path 路径
     * @return array
     * @throws Exception
     */
    static function getFileInfoList($path)
    {
        try {
            $dir = new \DirectoryIterator($path);
        } catch (\Exception $e) {
            throw $e;
        }
        $files = array();
        foreach ($dir as $file) {
            if ($file->isDot()) {
                continue;
            }
            $exten = $file->getExtension();
            $item = array(
                'name' => $file->getFileName(),
                'path' => $file->getPath(),
                'real_path' => $file->getRealPath(),
                'exten' => $exten,
                'filetype' => self::getExtenCatetoryName($exten),
                'mtime' => $file->getMTime(),
                'ctime' => $file->getCTime(),
                'size' => $file->getSize(),
                'is_dir' => $file->isDir(),
                'is_file' => $file->isFile(),
                'is_link' => $file->isLink(),
                'is_executable' => $file->isExecutable(),
                'is_readable' => $file->isReadable(),
                'is_writable' => $file->isWritable(),
            );
            $item['relative_path'] = self::path2ator($file->getRealPath());
            $item['filepath'] = $item['path'] . '/' . $item['name'];
            $files[] = $item;
        }
        return $files;
    }

    /**
     * 格式化文件字节大小
     * @param int $size
     * @return string
     */
    public static function formatBytes($size)
    {
        $units = array(' B', ' KB', ' MB', ' GB', ' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) {
            $size /= 1024;
        }
        return round($size, 2) . $units[$i];
    }
    /**
     * 获取文件扩展名类型
     * @param string $exten 扩展名(不带.)
     * @return string
     */
    public static function getExtenCatetoryName($exten)
    {
        if ($exten) {
            $filetypes = array(
                'zip' => array('zip', 'rar', '7-zip', 'tar', 'gz', 'gzip'),
                'doc' => array('txt', 'rtf', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'wps', 'et'),
                'script' => array('php', 'js', 'css', 'c'),
                'image' => array('jpg', 'jpeg', 'png', 'gif', 'tiff', 'psd', 'bmp', 'ico')
            );
            foreach ($filetypes as $catetory => $extens) {
                if (in_array($exten, $extens)) {
                    return $catetory;
                }
            }
        }
        return '';
    }
    /**
     * 绝对路径转相对路径
     * @param string $path
     * @return string
     */
    public static function path2ator($path)
    {
        $root = pathinfo($_SERVER['SCRIPT_FILENAME'], PATHINFO_DIRNAME);
        $path = substr($path, strlen($root));
        if ('/' != DIRECTORY_SEPARATOR) {
            $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
        }
        return $path;
    }

    /**
     * 相对路径转绝对文件路径
     * @param string $path
     * @return string
     */
    public static function path2rtoa($path)
    {
        $root = pathinfo($_SERVER['SCRIPT_FILENAME'], PATHINFO_DIRNAME);
        $pathstr = $root;
        $patharr = explode('/', $path);
        foreach ($patharr as $pathtmpstr) {
            if (!$pathtmpstr || $pathtmpstr == '.') {
                continue;
            }
            if ($pathtmpstr == '..') {
                $pathstr = pathinfo($pathstr, PATHINFO_DIRNAME);
                continue;
            }
            $pathstr .= '/' . $pathtmpstr;
        }

        return $pathstr . (is_dir($path) ? '/' : '');
    }
    /**
     * 自动创建路径中包含的多级目录
     * @param string  $path
     * @return string 目录路径
     */
    public static function mkdir($path)
    {
        // 文件或者目录如果已经存在则直接返回
        if (file_exists($path)) {
            return $path;
        }
        $ext = pathinfo($path, PATHINFO_EXTENSION);
        if ($ext) {
            $path = dirname($path);
            if (is_dir($path)) {
                return $path;
            }
        }
        mkdir($path, 0777, true);
        return $path;
    }

    /**
     * 删除非空目录里面所有文件和子目录
     * @param string $dir
     * @param boolean $delSelf 是否删除自身目录(删除还是清空)
     * @return boolean
     */
    public static function rmdir($dir, $delSelf = true)
    {
        //先删除目录下的文件：
        $dh = opendir($dir);
        while ($file = readdir($dh)) {
            if ($file != "." && $file != "..") {
                $fullpath = $dir . "/" . $file;
                if (is_dir($fullpath)) {
                    self::rmdir($fullpath, true);
                } else {
                    unlink($fullpath);
                }
            }
        }
        closedir($dh);
        //删除当前文件夹：
        return (!$delSelf || rmdir($dir));
    }

    /**
     * PHP 高效读取文件
     * return 改为直接 echo，否则大文件会显示空白，而且大文件整体读取不如直接用 file_get_contents 高效
     * @param string $filepath
     * @return string
     */
    public static function tail($filepath)
    {
        if (file_exists($filepath)) {
            if (false !== ($fp = fopen($filepath, "r"))) {
                $buffer = 1024; //每次读取 1024 字节
                while (!feof($fp)) { //循环读取，直至读取完整个文件
                    echo htmlspecialchars(fread($fp, $buffer));
                }
                fclose($fp);
            } else {
                echo 'file can not open! [' . $filepath . ']';
            }
        } else {
            echo 'file not exists! [' . $filepath . ']';
        }
    }

    /**
     * PHP 高效写入文件（支持并发）
     * @param string $filepath
     * @param string $content
     */
    static function writeFile($filepath, $content)
    {
        if ($fp = fopen($filepath, 'a')) {
            $startTime = microtime();
            // 对文件进行加锁时，设置一个超时时间为 1ms，如果这里时间内没有获得锁，就反复获得，直接获得到对文件操作权为止。
            // 当然，如果超时限制已到，就必需马上退出，让出锁让其它进程来进行操作。
            do {
                $canWrite = flock($fp, LOCK_EX);
                if (!$canWrite) {
                    usleep(round(rand(0, 100) * 1000));
                }
            } while ((!$canWrite) && ((microtime() - $startTime) < 1000));
            if ($canWrite) {
                fwrite($fp, $content);
            }
            fclose($fp);
        }
    }

    /**
     * 路径转为导航
     * @param string $path
     * @param string $rootpath
     * @param string $title
     */
    public static function showBreadcrumb($path, $rootpath = '', $title = 'location: ')
    {
        global $token;
        echo '<div class="nav">', $title, '<a href="?token=' . $token . '&path=', $rootpath, '">/</a>';
        $asubpath = explode('/', $path);
        if ($asubpath) {
            $str = '';
            $n = count($asubpath);
            $n2 = $n - 1;
            $rootlen = strlen($rootpath);
            for ($i = 0; $i < $n; $i++) {
                $sub = $asubpath[$i];
                $str .= $sub . ($i < $n2 ? '/' : '');
                if (substr($str, 0, $rootlen) != $rootpath) {
                    continue; // 防止显示../路径而把 ROOTPATH 以上的目录显示出来,造成安全问题
                }
                echo '<a href="?token=' . $token . '&path=', $str, '">', $sub, '</a>';
            }
        }
        echo '</div>';
    }
}
// ===========================公共函数===========================

/**
 * 检测 PHP 扩展是否可用
 * @param string $name
 * @return boolean
 */
function mysal_check_extension($name = 'PDO')
{
    return extension_loaded($name);
}
/**
 * 类自动加载功能
 */
function mysal_splautoload()
{
    // 启用自动加载类文件功能 new \app\seed\TestSeeder();
    spl_autoload_register(function ($class) {
        // 自动加载的命名空间的目录映射
        $dirMap = array(
            'mysal' => 'mysal',
        );
        // 解析类文件路径 mysal\stubs\fastlara\admin
        $vendor = substr($class, 0, strpos($class, '\\')); // 顶级命名空间
        $vendorDir = $dirMap[$vendor]; // 文件基目录
        $file = $vendorDir . substr($class, strlen($vendor)) . '.php'; // 文件相对路径
        /* 加载文件 */
        if (file_exists($file)) {
            include $file;
        }
    }, true, true);
}

/**
 * 截短字符串
 * @param string $str 字符串
 * @param integer $len 长度
 * @return string
 */
function mysal_short_text($str, $len)
{
    return mb_strlen($str) > $len ? (mb_substr($str, 0, $len) . '...') : $str;
}

/**
 * HTML 内容转换为纯文本
 * 例如：html2text($content,['/[\*|？|！|。|，]+/']);
 * @param string
 * @return
 */
function mysal_html2text($content, $expattern = [])
{
    // 将 html 实体符号（&nbsp; 等）还原成 html 标签 -> 过滤首尾空白字符 -> 过滤 html 标签
    $content = strip_tags(trim(html_entity_decode($content)));
    $pattern = [
        '/\s/', // 匹配任何空白字符，包括空格、制表符、换页符等，等价于 [\f\n\r\t\v]
    ];
    if ($expattern) {
        $pattern = array_merge($pattern, $expattern);
    }
    return preg_replace($pattern, '', $content);
}

/**
 * 数据表名称转换为类名
 * @param string $table 数据表名称
 */
function mysal_table2classname($table)
{
    return str_replace(' ', '', ucwords(str_replace('_', ' ', $table)));
}

/**
 * 返回二维数组中匹配的所有行
 * @param array $list
 * @param array $compareData
 * @param integer $limit
 * @return array
 */
function mysal_array_search_rows($list, $compareData, $limit = 1)
{
    $rows = [];
    if (!$list || !is_array($list)) {
        return $rows;
    }
    $n = count($compareData);
    $i = 0;
    foreach ($list as $k => $v) {
        foreach ($compareData as $field => $val) {
            if (isset($v[$field]) && $v[$field] == $val) {
                $i++;
            }
        }
        if ($i == $n) {
            $rows[$k] = $v;
            if ($limit > 0 && $i >= $limit) {
                break;
            }
        }
    }
    return $rows;
}
/**
 * 返回二维数组中指定列的值
 * @param array $list 二维数组
 * @param string $name  列名称
 * @param array 指定列的值的数组
 */
function mysal_array_column($list, $name)
{
    $col = [];
    if ($list) {
        foreach ($list as $row) {
            if (array_key_exists($name, $row)) {
                $col[] = $row[$name];
            }
        }
    }
    return $col;
}
/**
 * 迁移数组，把多维数组复制到另一个数组
 * 用于多维数组合并前的预处理
 * @param array $fromData
 * @param array &$toData
 */
function mysal_array_migrate($fromData, &$toData)
{
    foreach ($fromData as $key => $data) {
        if (!isset($toData[$key])) {
            $toData[$key] = $data;
            continue;
        }
        if (!$data || !is_array($data)) {
            continue;
        }
        mysal_array_migrate($data, $toData[$key]);
    }
}
/**
 * 获取数组中的值
 * @param array $array 数组
 * @param string $path 节点路径，例如：a.b.c
 * @param mixed $defv 默认值
 * @return mixed 返回数组中的值
 */
function mysal_array_value($array, $path, $defv = null)
{
    $paths = explode('.', $path);
    foreach ($paths as $node) {
        if (isset($array[$node])) {
            $array = $array[$node];
        } else {
            $array = $defv;
        }
    }
    return $array;
}
/**
 * 检测并且创建目录
 * @param string $pathname
 * @param integer $mode
 * @param boolean $recursive
 * @return boolean
 */
function mysal_ensure_dir($pathname, $mode = 0777, $recursive = false)
{
    if (!$pathname) {
        return false;
    }
    if (is_dir($pathname)) {
        return true;
    }
    return mkdir($pathname, $mode, $recursive);
}
/**
 * 返回版本日志
 */
function mysal_versionlog()
{
    return '
@version 1.0 <2017-8-23>
    sochishun/14507247@qq.com Added.
@version 1.1 <2017-8-28>
    新增批量语句查询、执行功能。
@version 1.2 <2017-9-6>
    新增导出数据库表结构为 Markdown 格式表格功能和过滤数据表前缀功能。
@version 1.3 <2017-9-20>
    为防止正式运营环境出现误操作的生产事故，在正式环境增加操作验证码功能(点击 3 次验证码输入框自动填写验证码)，测试服则关闭验证码。
@version 1.4 <2017-9-21>
    新增数据表快捷查询功能。
@version 1.5 <2017-9-23>
    新增服务器端口转发 rinetd 配置参数。
    新增数据库最新修改信息的查询。
    新增查询结果集导出到 MD 文件功能。
    新增侧边栏列表模块和查询结果窗口的收起和展开功能。
    新增服务器环境标题前缀提醒功能。
@version 1.6 <2017-9-29>
    新增结果集表格隐藏列功能。
@version 1.7 <2017-10-13>
    新增服务器 IP 显示，改进端口映射判断逻辑，如果同服务器则自动忽略端口映射以提高性能。
    修正 SQL 编辑器批量语句换行符识别错误的问题。
@version 1.8 <2017-10-14>
    数据表操作示例语句新增 desc，truncate，index，count，drop 等命令，并完善 alter 示例命令。
    侧边栏新增 “转到” 面板。
@version 1.9 <2017-10-16>
    新增存储过程创建语句的解析。
    SQL 编辑器初始面板新增创建各种数据库对象的示例语句。
    SQL 编辑器新增点击3次自动过滤单行注释功能。
@version 2.0 <2017-10-20>
    侧边栏新增快捷跳转面板。
@version 2.1 <2017-10-27>
    SQL 编辑器新增高危命令提醒功能。
@version 2.2 <2017-11-3>
    为适应负载均衡多台服务器分布式使用场景，登录会话保持由单机的 session 存储改为 url 参数 token。
    正式服 SQL 编辑器验证码增加鼠标点击五次自动填表功能。
@version 2.3 <2017-11-23>
    部分示例语句由跳转链接改为 JavaScript 动态显示在 SQL 编辑器，方便在查询中参照其他示例语句。
@version 2.4 <2017-11-24>
    新增测试数据批量生成功能。
@version 2.5 <2017-11-28>
    改进 token 功能，增加续期功能，避免频繁超时重新登录的繁琐操作。
    快速查询配置的 SQL 命令支持 {#object} 变量。
@version 2.6 <2017-12-1>
    新增导出到 SQL 文件功能。
@version 2.7 <2017-12-6>
    新增读取本地配置文件功能，如果有本地配置文件，则优先读取本地配置文件。
@version 2.8 <2017-12-15>
    登录方法封装成 JWTLite 类。
    配置信息处理方法封装成 ConfigHelper 类，方便管理和迁移。
    新增精简模式变量 litemode，方便在 iframe 中引用。
    SQL 编辑器支持 {#object} 变量，可自动替换为 table，routine 等。
    新增从 sqlstmt 解析对象名称的功能。
@version 2.9 <2018-1-10>
    新增导出到 Excel 文件功能。
    $quickquerysqlcfg 快捷命令新增 group-conf 配置项。
    SQL 编辑器初始面板新增数据库导入导出示例语句。
@version 3.0 <2020-1-13>
    适配支持 MySQL 8.0 以上版本。
    增加导出 HTML 文档功能。
@version 3.1 <2020-2-3>
    表格、按钮、功能面板等界面美化。
    新增数据表的对象模型展示功能。
@version 3.2 <2020-2-5>
    新增数据库服务器跳转功能
    新增导出表结构到 Phinx Migrate 迁移文件。
    新增导出表结构到 Phinx Seed 测试数据文件。
    新增导出创建表命令到 SQL 文件。
@version 3.3 <2020-2-12>
    SQL 编辑器增加复制清空等实用功能。
    顶部增加当前时间的信息。
    修改一些数据库信息的查询方法，兼容各版本 MySQL 数据库。
预告：下个版本将增加命令行功能，还在羡慕各种类似 Phinx 的数据迁移工具吗？一个mysqladminlite就能搞定，敬请期待！
@vertion 4.0 <2020-02-15>
    增加了命令行功能，很方便，很好用，以前是提供类似 mysqladmin 的功能，现在是在 mysqladmin 功能上再加上phinx数据迁移功能。
    命令行功能支持：1.导出 excel/html/sql 格式的文件； 2.导入 SQL 文件； 3.执行 SQL 查询命令； 4.查询数据库状态或变量； 
        5.生成测试数据。
@version 4.1 <2020-02-16>
    增加了命令行快接方式功能，详见bin目录(bin目录是为命令行模式使用的快捷方式示例，不影响单文件在线使用的便捷性)。
    支持在任何目录通过快接方式 [./mysal] 或 [php mysal] 使用本程序。
    优化了配置文件解析逻辑，支持外部配置文件优先。
@version 4.2 <2020-02-17>
    增加了命令行的多个批处理功能：source:all, seed:autocreate, seed:autorun。
    增加扩展功能函数命令 fnx。
@version 4.3 <2020-2-28>
    美化皮肤，按钮更加漂亮。
    增加了规范检测(phpcs)。
@version 5.0 <2020-3-1>
    界面重构，大量使用选项卡组件，界面更整洁，体验更舒适。
    增加 phpinfo 功能。
    增加教程网站快捷导航。
    重构 PHP 代码，增加类的命名空间，改善方法的专一性原则实践。
    增加脚本性能计时器。
    TODO:增加对 MariDB 的支持。
@version 5.0.1 <2020-3-2>
    增加数据表收藏夹功能，方便快速操作常用表。
@version 5.0.2 <2020-3-6>
    命令行增加了 export:data,export:database 用于数据库迁移操作。
    命令行扩展函数入口函数新增 $params 参数，用于接收命令行扩展函数参数。
    数据库迁移操作方法：
       1. export:database --database dbsample # 导出数据库表结构和表数据
       2. source --file dbsample.sql # 导入数据表架构迁移脚本
       3. source:all --dir data # 导入数据迁移脚本
@version 5.0.3 <2020-3-26>
    账号配置信息合并到全局配置中，方便在外部配置文件中自定义配置。
    数据库环境配置 extfile 节点增加 enable 属性，用于设置节点是否可用。
    命令行 sqlsample 命令更改为 template:sql。
    命令行新增 stub 自定义存根模板命令模块。
    查询结果视图新增在线查看或导出 json 格式文件的功能，可以方便生成 mock 模拟数据
    命令行 export:data 新增导出 excel,html,markdown,sql,json 格式文件功能
@version 5.0.4 <2020-4-20>
    全局配置文件新增 UI 静态资源配置。
    功能重构，单文件核心功能保留：数据库操作、导出功能、导入功能。
    扩展功能剥离到 Composer 扩展包，包括：扩展函数、数据迁移、种子数据、STUB 存根模板。
@version 5.0.5 <2020-4-28>
    重构：mysqladminlite.php 重构为：单文件独立版、Composer 全功能扩展包两个相互独立的项目。
    mysqladminlite.php 单文件独立版包含的功能不依赖任何第三方插件，功能包含：数据库操作、导入导出、数据库迁移、扩展函数。
    Composer 全功能扩展包功能包含：数据库操作、导入导出、数据迁移、STUB 存根模板、扩展函数等。Composer可以在任何项目使用，包括
        在 mysqladminlite 单文件独立版使用以便增强其功能。
';
}
